packages/deriver/tests/Unit/Deriver/Evaluator/Intrinsic/IntrinsicEvaluationTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Intrinsic;
6
7use Deriver\Deriver\Evaluator\Intrinsic\IntrinsicDescriptor;
8use Deriver\Deriver\Evaluator\Intrinsic\IntrinsicEvaluation;
9use Deriver\Deriver\Evaluator\Intrinsic\PureIntrinsic;
10use Deriver\Deriver\Evaluator\TargetProfile;
11use Deriver\Deriver\Evaluator\Term;
12use Deriver\Deriver\Evaluator\TermKind;
13use Deriver\Exception\EvaluationContractException;
14use Deriver\Exception\EvaluationException;
15use Override;
16use PHPUnit\Framework\Attributes\CoversClass;
17use PHPUnit\Framework\Attributes\Small;
18use PHPUnit\Framework\Attributes\UsesClass;
19use PHPUnit\Framework\TestCase;
20
21#[CoversClass(IntrinsicEvaluation::class)]
22#[UsesClass(EvaluationException::class)]
23#[UsesClass(IntrinsicDescriptor::class)]
24#[UsesClass(TargetProfile::class)]
25#[UsesClass(Term::class)]
26#[Small]
27final class IntrinsicEvaluationTest extends TestCase
28{
29    public function testEvaluateRejectsAnArityMismatchAsAContractFailure(): void
30    {
31        $this->expectException(EvaluationContractException::class);
32        (new IntrinsicEvaluation())->evaluate(new class ('compose') implements PureIntrinsic {
33            /**
34             * @param string $operation compose, override, or uncertain-tags
35             */
36            public function __construct(public readonly string $operation)
37            {
38            }
39            /**
40             * @return IntrinsicDescriptor Explicit input dependencies
41             */
42            #[Override]
43            public function descriptor(): IntrinsicDescriptor
44            {
45                return new IntrinsicDescriptor('example.' . $this->operation, '1', 'policy.' . $this->operation, 2, [0, 1]);
46            }
47            /**
48             * @param list<Term> $arguments Policy operands
49             * @param TargetProfile $target Declared semantics
50             * @return Term Structured policy with independent fields
51             */
52            #[Override]
53            public function evaluate(array $arguments, TargetProfile $target): Term
54            {
55                [$left, $right] = $arguments;
56                if ($left->kind !== TermKind::Array || $right->kind !== TermKind::Array) {
57                    return new Term(TermKind::Intrinsic, 'policy.' . $this->operation, $arguments, ['type' => 'array']);
58                }
59                if ($this->operation === 'override') {
60                    return Term::array(array_replace($left->operands, $right->operands));
61                }
62                $ttl = $left->operands['ttl'] ?? Term::opaque('UNKNOWN_TTL');
63                $other = $right->operands['ttl'] ?? $ttl;
64                $ttl = is_int($ttl->literal) && is_int($other->literal) ? Term::constant(min($ttl->literal, $other->literal)) : new Term(TermKind::Intrinsic, 'policy.minimum', [$ttl, $other]);
65                $tags = $this->operation === 'uncertain-tags' ? Term::opaque('UNKNOWN_TAG_TRANSFORM') : Term::array(array_values([...$left->operands['tags']->operands ?? [], ...$right->operands['tags']->operands ?? []]));
66                return Term::array(['ttl' => $ttl, 'tags' => $tags]);
67            }
68        }, [], new TargetProfile());
69    }
70    public function testEvaluatePropagatesTheOriginalFailure(): void
71    {
72        $intrinsic = self::createStub(PureIntrinsic::class);
73        $intrinsic->method('descriptor')->willReturn(new IntrinsicDescriptor('test', '1', 'test', 1, [0]));
74        $failure = new EvaluationException('intrinsic implementation failed');
75        $intrinsic->method('evaluate')->willThrowException($failure);
76        $this->expectExceptionObject($failure);
77        (new IntrinsicEvaluation())->evaluate($intrinsic, [Term::parameter('x')], new TargetProfile());
78    }
79    public function testEvaluateRejectsIncompleteArguments(): void
80    {
81        $this->expectException(EvaluationContractException::class);
82        (new IntrinsicEvaluation())->evaluate(new class ('compose') implements PureIntrinsic {
83            /**
84             * @param string $operation compose, override, or uncertain-tags
85             */
86            public function __construct(public readonly string $operation)
87            {
88            }
89            /**
90             * @return IntrinsicDescriptor Explicit input dependencies
91             */
92            #[Override]
93            public function descriptor(): IntrinsicDescriptor
94            {
95                return new IntrinsicDescriptor('example.' . $this->operation, '1', 'policy.' . $this->operation, 2, [0, 1]);
96            }
97            /**
98             * @param list<Term> $arguments Policy operands
99             * @param TargetProfile $target Declared semantics
100             * @return Term Structured policy with independent fields
101             */
102            #[Override]
103            public function evaluate(array $arguments, TargetProfile $target): Term
104            {
105                [$left, $right] = $arguments;
106                if ($left->kind !== TermKind::Array || $right->kind !== TermKind::Array) {
107                    return new Term(TermKind::Intrinsic, 'policy.' . $this->operation, $arguments, ['type' => 'array']);
108                }
109                if ($this->operation === 'override') {
110                    return Term::array(array_replace($left->operands, $right->operands));
111                }
112                $ttl = $left->operands['ttl'] ?? Term::opaque('UNKNOWN_TTL');
113                $other = $right->operands['ttl'] ?? $ttl;
114                $ttl = is_int($ttl->literal) && is_int($other->literal) ? Term::constant(min($ttl->literal, $other->literal)) : new Term(TermKind::Intrinsic, 'policy.minimum', [$ttl, $other]);
115                $tags = $this->operation === 'uncertain-tags' ? Term::opaque('UNKNOWN_TAG_TRANSFORM') : Term::array(array_values([...$left->operands['tags']->operands ?? [], ...$right->operands['tags']->operands ?? []]));
116                return Term::array(['ttl' => $ttl, 'tags' => $tags]);
117            }
118        }, [Term::parameter('x')], new TargetProfile());
119    }
120    public function testEvaluateReturnsTheIntrinsicResult(): void
121    {
122        $intrinsic = self::createStub(PureIntrinsic::class);
123        $intrinsic->method('descriptor')->willReturn(new IntrinsicDescriptor('test', '1', 'test', 1, [0]));
124        $intrinsic->method('evaluate')->willReturn(Term::constant('derived private text'));
125        $result = (new IntrinsicEvaluation())->evaluate($intrinsic, [Term::constant('private')], new TargetProfile());
126        self::assertSame('derived private text', $result->native());
127    }
128    public function testEvaluatePreservesTheReturnedTerm(): void
129    {
130        $intrinsic = self::createStub(PureIntrinsic::class);
131        $intrinsic->method('descriptor')->willReturn(new IntrinsicDescriptor('test', '1', 'test', 1, [0]));
132        $value = Term::constant('public');
133        $intrinsic->method('evaluate')->willReturn($value);
134        self::assertSame($value, (new IntrinsicEvaluation())->evaluate($intrinsic, [Term::constant('input')], new TargetProfile()));
135    }
136}
137