test(policy): fail if the schema advertises a field nothing ever supplies
Task 09 left this as its starred risk and deferred it to task 10, which then shipped without it. The failure mode is silent and expensive: an operator writes a rule on a field no call site puts in the context, activates it, and it never matches — no error, no log, and the clinic believes the rule is running. The test is structural rather than behavioural on purpose. Walking every real path for every field would need a test rig larger than the engine; asserting that each advertised field is populated somewhere in src/ catches the case that actually happens, which is a field added to the schema and nowhere else. Also closes the last few rows that had gone stale: - evaluateIsolated: PolicyResolver::evaluateOne() landed with the sandbox - forbid before candidate generation: the plan builder already reads prohibitions before the availability engine is reached - appointments.applied_policies and app:policy:seed-examples are declined with their reasons rather than left open — the trace lives on the price snapshot and a second column would be a second source of truth, and the template registry does the seeding job from inside the UI where the user can see the result before creating anything - the reserve list keeps its page in the URL like every other panel list Every checklist across the sixteen tasks now has zero pending rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Policy;
|
||||
|
||||
use App\Policy\Service\PolicySchema;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* هر فیلدی که schema تبلیغ میکند، باید جایی در کد **واقعاً پر شود**.
|
||||
*
|
||||
* خطر مشخص است و بیصداست: قانونی که روی فیلدی شرط بگذارد که هیچ فراخوانی آن را
|
||||
* نمیفرستد، هرگز مطابقت نمیکند و هیچ خطایی هم نمیدهد. اپراتور قانون را میسازد،
|
||||
* فعالش میکند، و تا ابد فکر میکند دارد کار میکند.
|
||||
*
|
||||
* این تست عمداً ساختاری است نه رفتاری: پیمایشِ همهٔ مسیرهای واقعی برای هر فیلد،
|
||||
* دستگاه تستی میخواست بزرگتر از خودِ موتور.
|
||||
*/
|
||||
class PolicyFieldCoverageTest extends TestCase
|
||||
{
|
||||
public function testEveryAdvertisedFieldIsSuppliedSomewhereInTheCode(): void
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
foreach (PolicySchema::FIELDS as $category => $list) {
|
||||
foreach ($list as $field) {
|
||||
$fields[$field][] = $category;
|
||||
}
|
||||
}
|
||||
|
||||
$sources = $this->sourceFiles(dirname(__DIR__, 2) . '/src');
|
||||
$missing = [];
|
||||
|
||||
foreach ($fields as $field => $categories) {
|
||||
$found = false;
|
||||
|
||||
foreach ($sources as $file => $code) {
|
||||
// خودِ schema فقط نام را اعلام میکند؛ پر کردنش جای دیگری است.
|
||||
if (str_ends_with($file, 'PolicySchema.php')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($code, sprintf("'%s'", $field)) && str_contains($code, '=>')) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$missing[$field] = $categories;
|
||||
}
|
||||
}
|
||||
|
||||
self::assertSame(
|
||||
[],
|
||||
$missing,
|
||||
'این فیلدها در schema هستند ولی هیچجا در context پر نمیشوند: ' .
|
||||
json_encode($missing, JSON_UNESCAPED_UNICODE),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, string> مسیر => محتوا */
|
||||
private function sourceFiles(string $root): array
|
||||
{
|
||||
$files = [];
|
||||
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root));
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$files[$file->getPathname()] = (string) file_get_contents($file->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user