# 07 — Phase 0: Isolation and Clean Baseline — Report

**Date:** 2026-11-03 → 2026-11-04
**Scope:** local, non-production isolation of the TechTitans POS copy at `C:\wamp64\www\techpos`.
Nothing was deployed or pushed.

**Status:** Phase 0 isolation checks **passed**.
- Backend: 3,137/3,137 passed in bounded batches (§4).
- Frontend: tsc and eslint clean (0 errors), 1,687/1,687 unit tests passed (§5).
- Unverified: Pint and the E2E suite (§6).

---

## 1. Files changed

### Backend repo (`techpos/backend`, branch `techtitans/main`)

| File | Change |
|---|---|
| `app/Support/Database/TechTitansDatabaseGuard.php` | **New.** Database-name allowlist guard. A database is allowed only if it is in `TECHTITANS_ALLOWED_DATABASES` **and** matches `^techtitans_[a-z0-9_]+$`. Test databases must also end in `_test`. |
| `app/Providers/AppServiceProvider.php` | Boot-time guard. `DB::prohibitDestructiveCommands()` applies unless the current DB is allowlisted. A `CommandStarting` listener refuses migrate/db:wipe/db:seed/schema:dump/queue/schedule/`cacart:*` provisioning against any non-allowlisted DB (including `--database=`). |
| `config/database.php` | Adds `techtitans_allowed_databases` (env `TECHTITANS_ALLOWED_DATABASES`). |
| `.env.example` | TechTitans template: app name, ports 8010/5180/8090, `techtitans_pos_dev`, dedicated user, unique cache/queue/session/Redis identities, log mail, no payment keys. |
| `phpunit.xml` | Forces `DB_DATABASE=techtitans_pos_test` and an allowlist of only `techtitans_pos_test`. Test-specific queue, cache and Redis prefixes. |
| `tests/TestCase.php`, `tests/Support/TestDatabaseGuard.php` | The test guard runs **before** `RefreshDatabase`. It requires `APP_ENV=testing`, MySQL, and an allowlisted `techtitans_*_test` DB. Anything else aborts before any schema operation. |
| `tests/Unit/TestDatabaseGuardTest.php`, `tests/Unit/Support/TechTitansDatabaseGuardTest.php` | Guard tests: `nh_pos_test`, `nh_pos_db`, `techpos`, the dev DB, look-alike names and an NH name that has been allowlisted are all rejected. |
| 9 `*ConcurrencyTest.php` files, 10 `tests/Support/*_worker.php` | Hard-coded `nh_pos_test` → `techtitans_pos_test`. The workers refuse any other DB. |
| `docs/techtitans/01–06` | Phase 1 audit documents, now tracked here. |
| `docs/techtitans/07-PHASE-0-ISOLATION-REPORT.md` | This report. |
| `docs/techtitans/08-WORDPRESS-CONNECTOR-REQUIREMENTS.md` | Missing v0.4.0 connector source; required interfaces. |
| `docs/techtitans/reference/techpos-nh-copy-schema.sql` | Schema-only snapshot of the protected `techpos` copy (no data, no credentials). |

Not tracked (gitignored): `backend/.env` (new APP_KEY, dedicated DB user). A local `.git/hooks/pre-push` blocks every push.

### Frontend repo (`techpos/frontend`, branch `techtitans/main`)

| File | Change |
|---|---|
| `.env.example` | TechTitans template: API `http://localhost:8010/api/v1`, Reverb port 8090. |
| `src/config/env.ts` | Default API fallback is 8010 (was NH's 8000). |
| `vite.config.ts` | Dev server pinned to 5180 (`strictPort`). Vitest `testTimeout: 15000`. |
| `playwright.config.ts`, 5 `e2e/*.spec.ts` | Base URL 5180; API `E2E_API_BASE_URL` or 8010. |
| `src/test/setup.ts` | `configure({ asyncUtilTimeout: 5000 })` (see §5). |
| `src/pages/LoginPage.test.tsx` | Login URL 8010. |
| `CustomerFinancialPanel.test.tsx`, `LocationsListPage.test.tsx`, `CategoryDetailPage.test.tsx` | Race fixes: synchronous `getBy*` on late-rendering elements replaced with `await findBy*`. |

Not tracked (gitignored): `frontend/.env.local`. A local `.git/hooks/pre-push` blocks every push.

**Not modified:** any file under `C:\wamp64\www\nh`, the `vmwebs-commerce` plugin, the `techpos` database, and any server-wide MySQL or PHP configuration.

---

## 2. Database and application isolation

### Databases

| Database | Role | State |
|---|---|---|
| `techpos` | Protected copy of NH data | **Unmodified.** See §7 |
| `techtitans_pos_dev` | TechTitans development | Was empty before migration. Now 194 migrations, 140 InnoDB tables, the `audit_logs_block_update` trigger, 75 permissions, and 1 synthetic tenant "TechTitans" (`owner@techtitans.invalid`, CAD, America/Toronto, location "TechTitans Main Store", 10 system roles). It holds 0 orders, customers, storefront connections, tokens, gateway configs or NH strings. |
| `techtitans_pos_test` | TechTitans tests only | Recreated by `RefreshDatabase`. Never pointed at anything else. |

`nh_pos_db` and `nh_pos_test` do not exist on this server and are rejected by name in every guard.

### MySQL account
- A dedicated user `techtitans_pos` (localhost and 127.0.0.1) has privileges **only** on `techtitans_pos_dev.*` and `techtitans_pos_test.*`.
- Verified: `SHOW DATABASES` lists only those two, and `SELECT` on `techpos.tenants` is **denied**.

### Guard (verified live)
- `migrate --pretend` against `techpos`, `nh_pos_db` and `nh_pos_test` was refused.
- `db:wipe --database` and `queue:work` against `techpos` were refused.

### Application identity (resolved config)

| Concern | TechTitans value |
|---|---|
| App | `TechTitans POS API`, `APP_URL=http://localhost:8010`, frontend `http://localhost:5180`, new APP_KEY (not copied) |
| DB | `mysql` → `techtitans_pos_dev` as `techtitans_pos` |
| Cache | `database` store, prefix `techtitans_pos_cache_` (tests: `array`, prefix `techtitans_pos_test_cache_`) |
| Queue | `database`, queue `techtitans-pos-default` (tests: `techtitans-pos-test`) |
| Session | `file`, cookie `techtitans_pos_session` (tests: `array`) |
| Redis | Not installed locally. Pre-isolated if enabled: prefix `techtitans_pos_` / `techtitans_pos_test_`, DB 7 (cache DB 8), queue `techtitans-pos-default` |
| Broadcast | `log`. Reverb port 8090 if enabled |
| Mail | `log`. From `no-reply@techtitans.invalid` |
| Payments | No Stripe, Bambora or Moneris keys. 0 gateway configs and 0 webhooks in the dev DB |
| WordPress | No WordPress tokens. 0 storefront connections in the dev DB |
| Scheduler / workers | None running. `queue:*` and `schedule:*` are refused against non-allowlisted DBs |

### Git
- In both repos, `origin` (NH Bitbucket) was **removed**. 0 remotes remain.
- Annotated tag `nh-baseline-2026-11-03` marks the NH HEAD: backend `61425fa`, frontend `74baeb4`.
- Work is on `techtitans/main`. Local `main` is preserved.
- A local `pre-push` hook rejects all pushes; this was verified with a push to a local bare repo.
- Nothing was pushed and no remote repository was created.

### Runtime check
- API on 8010: `/up` 200, `/api/v1/auth/me` 200 for the synthetic owner, 401 without a token.
- Frontend on 5180: 200, built with `VITE_API_BASE_URL=http://localhost:8010/api/v1`.
- CORS from `localhost:5180` is allowed, and authenticated cross-origin calls return 200.

---

## 3. Process cleanup

| Process | Finding | Action |
|---|---|---|
| PHPUnit PID 11424 (about 8 h) | No locks, no open InnoDB transactions, no child workers. Cause: see §4 | Stopped. Verified exited, 0 transactions, 0 connections |
| Port 8000: `artisan serve` 16280 → cmd 24468 → `php -S` 13484 | The copied project (`techpos/backend`), started from a VS Code terminal | Stopped (tree). Windows console processes accept only forced termination |
| Port 5173: `npm run dev` 3676 → 19148 → vite 20660 | The copied project (`techpos/frontend`). It had **hot-reloaded onto `[::1]:5180`** after the vite config change | Stopped (tree) |
| Leftover runner and `timeout` wrappers from the first batch attempt | Survived a task stop and overlapped with a probe run | Stopped; all PHPUnit processes verified gone |
| TechTitans dev servers 8010 and 5180 | Verification complete | Stopped |

**Result:** nothing listens on 8000, 5173, 8010, 5180 or 8090.

`techpos` showed no writes while the old 8000 server was alive: the latest `audit_logs.created_at` and token `last_used_at` are both 10:00, from the import.

---

## 4. Backend tests (`techtitans_pos_test` only)

### Why the full run took 8 hours (root cause)

1. **Xdebug** is loaded in CLI with `xdebug.mode=develop`. This made tests take 1.6–6 s each instead of about 0.3 s.
2. **Re-migration after DDL.** Some test classes run DDL or commit outside the test transaction, for example `RoleScopeMigrationAtomicityTest` and the `*ConcurrencyTest` classes that use worker processes. MySQL implicitly commits around DDL, so at teardown Laravel's `RefreshDatabase` finds no open transaction and resets `RefreshDatabaseState::$migrated` (`vendor/.../RefreshDatabase.php:157-159`). The **next test** then runs a full `migrate:fresh` (194 migrations, about 80–100 s here).
3. **Slow DDL and fsync** on this MySQL instance: `innodb_flush_log_at_trx_commit=1`, about 46 fsyncs/s at about 22 ms each.

There were no deadlocks, retries, or infinite loops. The suite's structure, combined with these environmental costs, made it effectively unbounded.

**Fix used:** run with `-d xdebug.mode=off -d opcache.enable_cli=1`, in bounded batches (each ≤ 570 s, `timeout --kill-after`). The DDL and concurrency classes run alone, one class per process.

### Batch results

| Batch | Directories | Result | Time |
|---|---|---|---|
| b1 core / auth / permissions | Unit, Auth, Authorization, Roles, Tenancy, Audit, Settings, Locations, Pin | **OK, 458 tests**, 1426 assertions | 197 s |
| b2 products | Menu | **OK, 765 tests**, 3088 assertions | 398 s |
| b3a inventory / POS sales | Inventory, Orders, Checkout | **OK, 354 tests**, 1345 assertions | 227 s |
| b3b payments / customers / devices | Payments, Customers, Tax, Kds, Stations, Printers | **OK, 249 tests**, 872 assertions | 171 s |
| b4 API / storefront | Storefront, Channels, Shipping, Platform, Mail, Install, Example | **OK, 202 tests**, 850 assertions | 163 s |
| b5a workforce | Workforce, TimeClock, Scheduling, CurrentAttendance | **OK, 496 tests**, 2659 assertions | 313 s |
| b5b attendance / AI | Attendance* (7 dirs), AI | **OK, 575 tests**, 4440 assertions | 256 s |
| **Subtotal** | Everything except the 12 isolated classes | **3,099 passed, 0 failed** | ~29 min |

### Isolated DDL / concurrency classes (one process each, 570 s cap)

| Class | Result | Time |
|---|---|---|
| Inventory/InventoryConcurrencyTest | **OK, 4 tests** | 105 s |
| Inventory/InventoryRestockConcurrencyTest | **OK, 3 tests** | 26 s |
| Orders/OrderConcurrencyTest | **OK, 5 tests** | 29 s |
| Authorization/RoleScopeMigrationAtomicityTest | **OK, 4 tests**, about 2 min per test because of re-migration. Close to the cap | 537 s |
| Platform/SupportSessionConcurrencyTest | **OK, 3 tests** | 107 s |
| AttendanceDevice/AttendanceDeviceConcurrencyTest | **OK, 2 tests** | 10 s |
| AttendanceKioskTimeClock/ConcurrencyTest | **OK, 4 tests** | 20 s |
| AttendanceVerification/ConsumerConcurrencyTest | **OK, 2 tests** | 9 s |
| Scheduling/ShiftConcurrencyTest | **OK, 3 tests** | 15 s |
| TimeClock/TimeClockConcurrencyTest | **OK, 4 tests** | 19 s |
| TimeClock/TimeClockCorrectionConcurrencyTest | **OK, 2 tests** | 10 s |
| Workforce/AttendancePin/AttendancePinConcurrencyTest | **OK, 2 tests** | 9 s |
| **Subtotal** | **38 passed, 0 failed** | ~15 min |

**Backend total: 3,137 tests passed, 0 failed, 0 timed out.** Every test file was covered across the batches. This was not a single run, and Pint is still unverified (§6). The worker processes connected only to `techtitans_pos_test`.

Logs and JUnit XML are kept per batch in the session scratchpad (`bt/*.log`, `bt/*.xml`, `bt/summary.txt`). They are not committed.

---

## 5. Frontend

| Check | Result |
|---|---|
| `tsc -b` | exit 0 |
| `eslint .` | 0 errors, 3 warnings, all pre-existing `react-hooks/exhaustive-deps` (`CataloguesPage.tsx:43`, `PaymentGatewaysPage.tsx:60`, `WebsiteConnectionsPage.tsx:34`) |
| Vitest, bounded batches | `src/pages/menu` 262/262. `src/pages` (other) 549/550, the one failure fixed below. Non-page `src/*` 875/875. **1,687 tests total** |
| Timing-sensitive files, re-run after fixes | 5 files, **79/79**. `CategoryDetailPage` alone 29/29 |
| Final full run after all fixes (one bounded run, no competing load) | **164 files, 1,687/1,687 passed**, 185 s |

### The original 2 timeouts and related flakes

- **RestaurantProfilePage (×2, original):** the full app boot (auth → `/me` → `/context` → lazy route) exceeded Testing Library's 1000 ms `findBy*` default under 24 parallel workers. Fix: global `asyncUtilTimeout: 5000`. This only extends polling, so a genuinely missing element still fails.
- **CustomerFinancialPanel, LocationsListPage, CategoryDetailPage (×2):** a synchronous `getBy*` was used immediately after an async render or tab switch. These were real test races and are fixed with `await findBy*`.
- **EmployeeProfilePage "deep-links to the full schedule":** this test chains boot, a click, and a second lazy route. It hit Vitest's 5000 ms per-test default, which equalled the new `asyncUtilTimeout`. Fix: `testTimeout: 15000`.

No application code was changed for any of these.

### E2E (Playwright)
- **Not run.** The specs still use NH fixtures and credentials (`kings-biryani`, NH users) and header comments about `nh_pos_db`.
- They must not run until those fixtures are replaced with TechTitans synthetic fixtures.

---

## 6. Remaining failures, unverified items, blockers

| Item | Status |
|---|---|
| Isolated DDL / concurrency classes | All 12 passed (§4). `RoleScopeMigrationAtomicityTest` is slow (537 s) |
| Full suite in one process | **Not claimed.** All 3,137 tests passed across bounded batches, but not in one process |
| Laravel Pint `--test` | Not completed: it hung (more than 1 h). Unverified |
| E2E Playwright | Not run (NH fixtures) |
| Redis | Not installed. The Redis isolation settings are configured but not exercised |
| `cacart:dev-bootstrap` | Still seeds NH demo data ("Kings Biryani"). It is guarded, but **do not use it**. Use `cacart:provision-tenant` |
| WordPress connector v0.4.0 source | **Missing.** See `08`. This blocks connector work only, not inventory |
| Branding | UI strings still say NH POS / Cacart in places. To do in Phase 1 |
| Test-speed recommendations | (a) Disable Xdebug in CLI, or use `-d xdebug.mode=off` in a composer `test` script. (b) Make the DDL tests restore `RefreshDatabaseState` cheaply, or move them to a separate PHPUnit suite. (c) For a dev-only MySQL, consider `innodb_flush_log_at_trx_commit=2`. This is **not applied** because the server also hosts `techpos` and other applications' databases |

**No critical isolation issue remains.**

---

## 7. NH not modified

- **`techpos` database:** `CHECKSUM TABLE` before and after Phase 0 are identical.

  | Table | Checksum |
  |---|---|
  | audit_logs | 1125793426 |
  | orders | 3609865123 |
  | customers | 1578354414 |
  | storefront_connection_tokens | 2742964780 |
  | payment_gateway_configs | 957890130 |
  | users | 3626319799 |

  Also unchanged: 140 tables, about 1533 rows, 194 migrations, 1 tenant, 6 storefront connections, 1 gateway config, and the latest audit and token-use timestamps (10:00, import time).
- **NH WordPress and the plugin:** `C:\wamp64\www\nh\wp-content\plugins\vmwebs-commerce` has a clean working tree (HEAD `86cbb71`, 0 remotes). **0 files** under `C:\wamp64\www\nh` are newer than the SQL import.
- **NH repositories:** never contacted, except as noted. The only network git action was a `push --dry-run` to a dummy Bitbucket URL, which failed authentication before sending anything. No push was made anywhere. The originals are not on this machine.
- **Services:** no NH payment, webhook, WordPress or email endpoint is configured. No scheduler or worker was started.

---

## 8. Readiness for Inventory Increment 1.0

**Ready**, with these conditions:

- Work only on `techtitans/main` against `techtitans_pos_dev` and `techtitans_pos_test`. The guards make any other target fail closed.
- Run backend tests with Xdebug off and in bounded batches, as in §4. Inventory-scoped runs (`tests/Feature/Inventory`) take about 2–4 min.
- Track the open items in §6. None of them blocks Inventory Increment 1.0.
- Inventory Increment 1.0 has **not** been started.
