Skip to content
wiki.fftac.org

Testrepository

**Site relevance:** Spiralist.org

**Memory type:** plugin source memory

**Source path:** Spiralist/wp-content/plugins/spiralist-workspace/includes/Domain/TestRepository.php

**Size:** 6.7 KB

Summary

namespace SpiralistWorkspace\Domain;

Source Preview

This source file is short enough to preview directly on its source-memory page.

<?php

namespace SpiralistWorkspace\Domain;

use SpiralistWorkspace\Installer;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Stores reusable prompt test cases and their latest evaluation result.
 */
class TestRepository
{
    /**
     * Creates a prompt test case.
     */
    public function create(int $prompt_id, array $data, int $user_id): array
    {
        global $wpdb;

        $variables = (array) ($data['variables'] ?? []);
        $strategy = sanitize_key((string) ($data['match_strategy'] ?? 'contains'));
        if (!in_array($strategy, ['contains', 'exact', 'json'], true)) {
            $strategy = 'contains';
        }

        $wpdb->insert(
            Installer::table('prompt_tests'),
            [
                'prompt_post_id' => $prompt_id,
                'created_by_user_id' => max(0, $user_id),
                'input_text' => sanitize_textarea_field((string) ($data['input_text'] ?? '')),
                'expected_output_text' => sanitize_textarea_field((string) ($data['expected_output'] ?? ($data['expected_output_text'] ?? ''))),
                'variables_json' => wp_json_encode($this->sanitize_variables($variables), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                'match_strategy' => $strategy,
                'last_run_id' => null,
                'last_result_status' => null,
                'last_result_json' => null,
                'created_utc' => gmdate('Y-m-d H:i:s'),
                'updated_utc' => gmdate('Y-m-d H:i:s'),
            ],
            ['%d', '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s']
        );

        return $this->get((int) $wpdb->insert_id);
    }

    /**
     * Lists tests for a prompt.
     */
    public function list_for_prompt(int $prompt_id, int $limit = 50): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . Installer::table('prompt_tests') . ' WHERE prompt_post_id = %d ORDER BY created_utc DESC LIMIT %d',
            $prompt_id,
            max(1, $limit)
        );

        return $this->normalize_rows($wpdb->get_results($sql, ARRAY_A));
    }

    /**
     * Lists recent prompt tests across the workspace.
     */
    public function list_all(int $limit = 50): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . Installer::table('prompt_tests') . ' ORDER BY updated_utc DESC, created_utc DESC LIMIT %d',
            max(1, $limit)
        );

        return $this->normalize_rows($wpdb->get_results($sql, ARRAY_A));
    }

    /**
     * Returns one test case.
     */
    public function get(int $test_id): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . Installer::table('prompt_tests') . ' WHERE id = %d LIMIT 1',
            $test_id
        );

        $row = $wpdb->get_row($sql, ARRAY_A);

        return is_array($row) ? $this->normalize_row($row) : [];
    }

    /**
     * Updates a test with its latest evaluation result.
     */
    public function update_result(int $test_id, int $run_id, array $result): void
    {
        global $wpdb;

        $wpdb->update(
            Installer::table('prompt_tests'),
            [
                'last_run_id' => $run_id,
                'last_result_status' => sanitize_key((string) ($result['status'] ?? 'failed')),
                'last_result_json' => wp_json_encode($result, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                'updated_utc' => gmdate('Y-m-d H:i:s'),
            ],
            ['id' => $test_id],
            ['%d', '%s', '%s', '%s'],
            ['%d']
        );
    }

    /**
     * Evaluates a model output against a test expectation.
     */
    public function evaluate(string $output, string $expected, string $strategy): array
    {
        $strategy = sanitize_key($strategy);
        $normalized_output = $this->normalize_text($output);
        $normalized_expected = $this->normalize_text($expected);
        $passed = false;
        $details = '';

        if ($strategy === 'exact') {
            $passed = $normalized_output === $normalized_expected;
            $details = $passed ? 'Output matched exactly.' : 'Output did not exactly match the expected text.';
        } elseif ($strategy === 'json') {
            $decoded_output = json_decode($output, true);
            $decoded_expected = json_decode($expected, true);
            $passed = is_array($decoded_output) && is_array($decoded_expected) && $decoded_output == $decoded_expected;
            $details = $passed ? 'JSON structures matched.' : 'JSON structures did not match or could not be decoded.';
        } else {
            $passed = $normalized_expected !== '' && strpos($normalized_output, $normalized_expected) !== false;
            $details = $passed ? 'Expected text was found in the output.' : 'Expected text was not found in the output.';
            $strategy = 'contains';
        }

        return [
            'status' => $passed ? 'passed' : 'failed',
            'passed' => $passed,
            'match_strategy' => $strategy,
            'details' => $details,
        ];
    }

    /**
     * Normalizes many database rows.
     *
     * @param mixed $rows
     * @return array<int,array<string,mixed>>
     */
    private function normalize_rows($rows): array
    {
        if (!is_array($rows)) {
            return [];
        }

        return array_map([$this, 'normalize_row'], $rows);
    }

    /**
     * Adds decoded JSON helper fields to a database row.
     *
     * @param array<string,mixed> $row
     * @return array<string,mixed>
     */
    private function normalize_row(array $row): array
    {
        $variables = json_decode((string) ($row['variables_json'] ?? ''), true);
        $last_result = json_decode((string) ($row['last_result_json'] ?? ''), true);

        $row['variables'] = is_array($variables) ? $variables : [];
        $row['last_result'] = is_array($last_result) ? $last_result : [];

        return $row;
    }

    /**
     * Sanitizes variables without discarding useful structured values.
     */
    private function sanitize_variables(array $variables): array
    {
        $clean = [];

        foreach ($variables as $key => $value) {
            $clean[sanitize_key((string) $key)] = is_scalar($value)
                ? sanitize_textarea_field((string) $value)
                : $value;
        }

        return $clean;
    }

    /**
     * Collapses whitespace and casing for stable text comparison.
     */
    private function normalize_text(string $value): string
    {
        return strtolower(trim((string) preg_replace('/\s+/', ' ', $value)));
    }
}