renderer = new PromptRenderer(); } public function test_extract_variables_returns_empty_list_without_variables(): void { $this->assertSame([], $this->renderer->extractVariables('')); $this->assertSame([], $this->renderer->extractVariables('Plain text, no placeholders at all.')); } public function test_extract_variables_returns_unique_names_in_first_occurrence_order(): void { $body = 'Hello {{name}}, here is {{content}}. Again: {{name}} and {{extra}}.'; $this->assertSame(['name', 'content', 'extra'], $this->renderer->extractVariables($body)); } public function test_extract_variables_tolerates_inner_whitespace(): void { $this->assertSame( ['content'], $this->renderer->extractVariables('{{content}} {{ content }}') ); $this->assertSame( ['a_b', 'A-B'], $this->renderer->extractVariables('{{a_b}} and {{ A-B }}') ); // `$` is not in the variable charset, so this stays literal. $this->assertSame([], $this->renderer->extractVariables('{{$a_b}}')); } public function test_invalid_tokens_are_not_treated_as_variables(): void { $body = '{{}} and {{a b}} and {{a!b}} and {{ }} stay literal'; $this->assertSame([], $this->renderer->extractVariables($body)); } public function test_render_replaces_every_occurrence(): void { $rendered = $this->renderer->render( 'Hi {{name}}! Send {{name}} the file.', ['name' => 'Ada'] ); $this->assertSame('Hi Ada! Send Ada the file.', $rendered); } public function test_render_returns_body_unchanged_without_variables(): void { $body = 'Nothing to do here.'; $this->assertSame($body, $this->renderer->render($body, ['ignored' => 'x'])); } public function test_render_is_strict_about_missing_variables(): void { try { $this->renderer->render('A: {{alpha}} B: {{beta}} C: {{gamma}}', ['beta' => 'b']); $this->fail('Expected PromptRenderException was not thrown.'); } catch (PromptRenderException $exception) { $this->assertSame(['alpha', 'gamma'], $exception->missingVariables); $this->assertSame('Missing variables: alpha, gamma', $exception->getMessage()); } } public function test_render_treats_non_string_values_as_missing(): void { try { $this->renderer->render('Count: {{count}}', ['count' => 42]); $this->fail('Expected PromptRenderException was not thrown.'); } catch (PromptRenderException $exception) { $this->assertSame(['count'], $exception->missingVariables); } try { $this->renderer->render('Count: {{count}}', ['count' => null]); $this->fail('Expected PromptRenderException was not thrown.'); } catch (PromptRenderException $exception) { $this->assertSame(['count'], $exception->missingVariables); } } public function test_render_accepts_empty_string_values(): void { $this->assertSame( 'Title: ', $this->renderer->render('Title: {{title}}', ['title' => '']) ); } public function test_render_ignores_extra_values(): void { $rendered = $this->renderer->render( 'Only {{used}}', ['used' => 'yes', 'extra' => 'no'] ); $this->assertSame('Only yes', $rendered); } public function test_inserted_values_are_not_re_scanned_for_variables(): void { $rendered = $this->renderer->render( 'Hello {{name}}, and {{other}}.', ['name' => '{{other}}', 'other' => 'world'] ); $this->assertSame('Hello {{other}}, and world.', $rendered); } public function test_missing_variables_are_reported_in_template_order(): void { try { $this->renderer->render('{{zulu}} {{alpha}} {{zulu}}', []); $this->fail('Expected PromptRenderException was not thrown.'); } catch (PromptRenderException $exception) { $this->assertSame(['zulu', 'alpha'], $exception->missingVariables); } } }