Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
TIGER_TEST_DB_PASS: root
# The floor we enforce today (report-only warning above it). Bump this as coverage climbs so it
# can only go UP — the ratchet that gets us to 90% without ever regressing.
MIN_COVERAGE: '18'
MIN_COVERAGE: '35'
steps:
- uses: actions/checkout@v4

Expand Down
27 changes: 18 additions & 9 deletions library/Tiger/Db/Migrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,26 @@ class Tiger_Db_Migrator
/** @var string[] directories to scan for migration files, in precedence order */
private $paths;

/** @var string ledger table recording applied versions (default `tiger_migration`) */
private $ledger;

/**
* Construct the migrator over a DB adapter and a set of migration directories.
*
* @param Zend_Db_Adapter_Abstract $db
* @param string[] $paths migration directories (existing ones; missing are ignored)
* @param string[] $paths migration directories (existing ones; missing are ignored)
* @param string $ledgerTable the applied-versions ledger table. Defaults to the
* canonical `tiger_migration`; override it to run a migrator
* against an ISOLATED ledger (so a migration test's fixture
* versions never collide with — or roll back — the real
* schema's applied set).
* @return void
*/
public function __construct(Zend_Db_Adapter_Abstract $db, array $paths)
public function __construct(Zend_Db_Adapter_Abstract $db, array $paths, $ledgerTable = 'tiger_migration')
{
$this->db = $db;
$this->paths = array_values(array_filter($paths, 'is_dir'));
$this->db = $db;
$this->paths = array_values(array_filter($paths, 'is_dir'));
$this->ledger = (string) $ledgerTable;
}

/**
Expand All @@ -68,7 +77,7 @@ public function migrate($log = null)
$this->run($stmt);
}
// Record only after every statement succeeded (see class caveat).
$this->db->insert('tiger_migration', [
$this->db->insert($this->ledger, [
'version' => $version,
'name' => $m['name'],
'applied_at' => date('Y-m-d H:i:s'),
Expand Down Expand Up @@ -106,7 +115,7 @@ public function rollback($steps = 1, $log = null)
foreach ($m['down'] as $stmt) {
$this->run($stmt);
}
$this->db->delete('tiger_migration', $this->db->quoteInto('version = ?', $version));
$this->db->delete($this->ledger, $this->db->quoteInto('version = ?', $version));
$done[$version] = $m['name'];
}
return $done;
Expand Down Expand Up @@ -179,20 +188,20 @@ private function discover()
/** @return array [version => true] of already-applied versions */
private function appliedVersions()
{
$rows = $this->db->fetchCol('SELECT version FROM tiger_migration');
$rows = $this->db->fetchCol('SELECT version FROM ' . $this->db->quoteIdentifier($this->ledger));
return array_fill_keys($rows, true);
}

/** Create the tracking table if it doesn't exist. */
private function ensureTrackingTable()
{
$this->db->query(
"CREATE TABLE IF NOT EXISTS `tiger_migration` (
'CREATE TABLE IF NOT EXISTS ' . $this->db->quoteIdentifier($this->ledger) . ' (
`version` VARCHAR(64) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`applied_at` DATETIME NOT NULL,
PRIMARY KEY (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
);
}

Expand Down
31 changes: 24 additions & 7 deletions tests/COVERAGE-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,13 +578,30 @@ Then **4 parallel agents** (own worktree + own DB `tiger_test_w3a-d`, off `test/
lines** (2863/14397) — the tested spine is ~100%, the drag is untested feature modules (blog/seo/media/
profile/analytics/backup/identity/schedule at 0%) + the marketplace/install cluster. **Target: 90%.**

### Next waves (priority order per §5/§8) — the drive to 90%
- **Wave 4 — breadth over the zero-coverage core modules** (now unblocked by the savepoint mode): each is
small — media, profile, identity, blog, seo, analytics, backup, schedule, ally, search, agent — parallel
agents, one module (or a cluster) each, service + model + controller coverage. Biggest, fastest % gain.
- **Wave 5 — finish the kernel install/marketplace cluster** (`Module_Installer`, `Vendor`, `Update_Checker`,
`Module_Github`, `Module_Dependency`) — the largest untested library chunk, security-adjacent.
- **Wave 6 — satellite repos:** stand up a harness in each, then TigerShield WAF engines first.
### Wave 4 — module breadth + install cluster (LANDED 2026-07-24) → coverage 20.5% → 37.9%
6 parallel agents (own worktree + DB each), **+316 integration tests** (265 → 581 green) + install-cluster unit
tests. Per-bucket line-%: **ally 100 · seo 88 · blog 83 · signup 87 · search 73 · access 69 · schedule 62 ·
profile 56 · identity 45 · analytics 41 · media 34 · backup 31 · system 37**; the kernel install/marketplace
cluster ~2–8×'d (`Dependency` 97 · `License_Authority` 94 · `Vendor` 80 · `Update_Checker` 79 · `Installer` 68 ·
`Github` 66) lifting **library/Tiger 25 → 32%**. No product bugs found. CI floor `MIN_COVERAGE` 18 → 35.
- **Real fix at collection:** `Tiger_Db_Migrator` gained an optional **ledger-table** arg (default
`tiger_migration`, prod unchanged); `MigratorTest` now uses an ISOLATED ledger. Root cause: `rollback()`
reverses the newest applied version GLOBALLY, and a module's timestamp-versioned migration (committed past
the per-test rollback — DDL auto-commits — by the install lifecycle test) sorted above the `9xxx` fixtures →
a cross-test flake. Hermetic ledger kills it.
- **Recurring ceiling (not bugs):** `is_uploaded_file()` gates upload happy-paths (profile Avatar/OrgLogo,
media upload) → unreachable from CLI; render-only controllers + `exit;`-ending file/serve actions need a
full MVC/view boot the harness skips. These are functional/HTTP-test concerns — the % gaps below ~90 on
media/profile/backup are mostly these, not missing logic.

### Next waves (priority order) — the drive to 90%
- **Wave 5 — the library/Tiger KERNEL** (9,176 lines @ 32% = the dominant remaining gap, ~6,200 uncovered):
the CMS engine (`Cms_*`, `Menu`, `Page*` finders), `Mail`, `Log`, `Location` adapters, `Admin_*` registries,
`Form`/`Validate`/`Form_Element_*`, `Ajax_ServiceFactory` remainder, and the untested branches of the Model
+ Service classes. This is what actually moves the overall number to the 80s. Parallel agents by sub-package.
- **Wave 6 — the remaining modules:** `agent` (462 @ 5% — needs a provider-adapter stub), `cms` module (510 @
33%), `code` (238 @ 41%), and `core/controllers` (360 @ 0% — needs the controller-dispatch harness).
- **Wave 7 — satellite repos:** stand up a harness in each, then TigerShield WAF engines first.

---
*Manifest generated 2026-07-18 by 6 parallel read-only scans; graduated into `tests/COVERAGE-PLAN.md` +
Expand Down
66 changes: 66 additions & 0 deletions tests/Integration/Ally/BootstrapTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.

namespace Tiger\Tests\Integration\Ally;

use Ally_Bootstrap;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use ReflectionMethod;
use ReflectionProperty;
use Tiger\Tests\Support\IntegrationTestCase;
use Tiger_Admin_Nav;

/**
* Ally_Bootstrap — registers the top-level "Accessibility" admin sidebar item. The hook touches only
* the process-wide Tiger_Admin_Nav registry (no $this state), so it's invoked on a constructor-less
* instance via reflection and the registration effect asserted.
*/
#[CoversClass(Ally_Bootstrap::class)]
final class BootstrapTest extends IntegrationTestCase
{
public static function setUpBeforeClass(): void
{
// The bare `Ally_Bootstrap` isn't a typed module class (the harness autoloader only resolves
// controllers/services/forms/models/plugins), so require it directly.
if (!class_exists(Ally_Bootstrap::class, false)) {
require_once TIGER_CORE_PATH . '/modules/ally/Bootstrap.php';
}
}

protected function tearDown(): void
{
Tiger_Admin_Nav::clear();
parent::tearDown();
}

/** The registered nav items keyed by 'key', read straight from the registry (no ACL filtering). */
private function registered(): array
{
$p = new ReflectionProperty(Tiger_Admin_Nav::class, '_items');
$p->setAccessible(true);
$items = [];
foreach ((array) $p->getValue() as $item) {
$items[$item['key'] ?? ''] = $item;
}
return $items;
}

#[Test]
public function it_registers_the_accessibility_sidebar_item(): void
{
Tiger_Admin_Nav::clear();

$bootstrap = (new \ReflectionClass(Ally_Bootstrap::class))->newInstanceWithoutConstructor();
$m = new ReflectionMethod(Ally_Bootstrap::class, '_initAdminNav');
$m->setAccessible(true);
$m->invoke($bootstrap);

$items = $this->registered();
$this->assertArrayHasKey('ally', $items, 'the Accessibility nav item is registered');
$this->assertSame('Accessibility', $items['ally']['label']);
$this->assertSame('/ally', $items['ally']['href']);
$this->assertSame('Ally_IndexController', $items['ally']['resource'], 'ACL-gated to the controller');
}
}
73 changes: 73 additions & 0 deletions tests/Integration/Ally/IndexControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.

namespace Tiger\Tests\Integration\Ally;

use Ally_IndexController;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tiger\Tests\Support\IntegrationTestCase;
use Zend_Controller_Front;
use Zend_Controller_Request_Http;
use Zend_Controller_Response_Http;
use Zend_Layout;
use Zend_Registry;
use Zend_Session;
use Zend_View;

/**
* Ally_IndexController — the (thin) admin screen for the accessibility inspector. It renders the tool;
* every scan is an /api call. init() sets the admin layout + the flash messenger; indexAction just
* seeds the page title.
*
* The test stands up the minimal admin-controller context (front controller module dir + Zend_Layout +
* an array-backed session for the FlashMessenger) so the real init() runs, then asserts the action.
*/
#[CoversClass(Ally_IndexController::class)]
final class IndexControllerTest extends IntegrationTestCase
{
private bool $priorUnitTestMode;

protected function setUp(): void
{
parent::setUp();

$this->priorUnitTestMode = Zend_Session::$_unitTestEnabled;
Zend_Session::$_unitTestEnabled = true; // array-backed session so FlashMessenger runs under CLI
$_SESSION = [];

Zend_Registry::set('Zend_View', new Zend_View());
Zend_Controller_Front::getInstance()->addControllerDirectory(TIGER_CORE_PATH . '/modules/ally/controllers', 'ally');
Zend_Layout::startMvc();
}

protected function tearDown(): void
{
Zend_Layout::resetMvcInstance();
Zend_Controller_Front::getInstance()->resetInstance();
$_SESSION = [];
Zend_Session::$_unitTestEnabled = $this->priorUnitTestMode;
$reg = Zend_Registry::getInstance();
if ($reg->offsetExists('Zend_View')) { $reg->offsetUnset('Zend_View'); }
parent::tearDown();
}

#[Test]
public function index_action_sets_the_page_title(): void
{
$req = new Zend_Controller_Request_Http();
$req->setModuleName('ally')->setControllerName('index')->setActionName('index');
$res = new Zend_Controller_Response_Http();

$ctrl = new Ally_IndexController($req, $res);
// Touch the ViewRenderer so it initializes the controller's view (a real dispatch does this
// before the action runs); indexAction seeds $this->view->title.
$ctrl->getHelper('viewRenderer');
$ctrl->indexAction();

$this->assertStringContainsString('Accessibility', (string) $ctrl->view->title);
// init() set the admin layout for the shell.
$this->assertSame('admin', $ctrl->getHelper('layout')->getLayout());
}
}
Loading
Loading