not->toBeFalse('Failed to read lib/rrd.php'); $pattern = '/if\s*\(\s*\$cdef_string\s*===\s*\'\'\s*\)\s*\{[^}]*continue;/s'; expect(preg_match($pattern, $src))->toBe(1, 'the empty cdef guard must stay in lib/rrd.php while totalling covers GPRINT items'); }); test('totalling replaces CURRENT_DATA_SOURCE for both total types', function () use ($root) { $src = file_get_contents($root . '/lib/aggregate.php'); expect($src)->not->toBeFalse('Failed to read lib/aggregate.php'); foreach (['SIMILAR_DATA_SOURCES_NODUPS', 'ALL_DATA_SOURCES_NODUPS'] as $replacement) { $pattern = "/str_replace\('CURRENT_DATA_SOURCE',\s*'" . $replacement . "'/"; expect(preg_match($pattern, $src))->toBe(1, "totalling must build the $replacement cdef"); } }); test('totalling query and cdef update operate on the same item set', function () use ($root) { $src = file_get_contents($root . '/lib/aggregate.php'); expect($src)->not->toBeFalse('Failed to read lib/aggregate.php'); $fnPos = strpos($src, 'function aggregate_cdef_totalling('); expect($fnPos)->not->toBeFalse('aggregate_cdef_totalling must exist'); $fnEnd = strpos($src, "\nfunction ", $fnPos + 1); $body = substr($src, $fnPos, ($fnEnd === false ? strlen($src) : $fnEnd) - $fnPos); // every selected item gets the new cdef; no filtering between select and update expect(strpos($body, 'SELECT id, cdef_id'))->not->toBeFalse('item selection missing'); $pattern = '/UPDATE graph_templates_item\s+SET cdef_id\s*=\s*\?\s+WHERE id\s*=\s*\?/s'; expect(preg_match($pattern, $body))->toBe(1, 'cdef update must remain prepared and keyed by item id'); }); test('aggregate caller rolls back and surfaces CDEF totalling failures', function () use ($root) { $src = file_get_contents($root . '/lib/api_aggregate.php'); expect($src)->not->toBeFalse('Failed to read lib/api_aggregate.php'); $fnPos = strpos($src, 'function aggregate_create_update('); expect($fnPos)->not->toBeFalse('aggregate_create_update must exist'); $fnEnd = strpos($src, "\nfunction ", $fnPos + 1); $body = substr($src, $fnPos, ($fnEnd === false ? strlen($src) : $fnEnd) - $fnPos); expect($body)->toContain('db_begin_transaction()') ->and($body)->toContain('if (!aggregate_cdef_totalling(') ->and($body)->toContain('db_rollback_transaction()') ->and($body)->toContain("raise_message('aggregate_invalid_cdef'") ->and($body)->toContain('db_commit_transaction()'); }); test('aggregate callers preserve existing rows and honor update failures', function () use ($root) { $api = file_get_contents($root . '/lib/api_aggregate.php'); $graphs = file_get_contents($root . '/graphs.php'); expect($api)->not->toBeFalse() ->and($graphs)->not->toBeFalse(); $pushStart = strpos($api, 'function push_out_aggregates('); $pushEnd = strpos($api, "\nfunction ", $pushStart + 1); $pushBody = substr($api, $pushStart, $pushEnd - $pushStart); expect($pushBody)->not->toContain('DELETE FROM graph_templates_item') ->and($pushBody)->toContain('if (!aggregate_create_update(') ->and($pushBody)->toContain('return false;') ->and($graphs)->toContain('if (!aggregate_create_update($local_graph_id, $member_graphs, $attribs, false))') ->and($graphs)->toContain('db_rollback_transaction()') ->and($graphs)->toContain('db_commit_transaction()'); });