From a1dc29286e06913ad35845978d57aa1eb31a6a7a Mon Sep 17 00:00:00 2001 From: Delta Team Date: Thu, 25 Jun 2026 15:56:53 -0400 Subject: [PATCH] PR_26175_DELTA_007: add runtime API client service tests --- ...TA_007-runtime-api-client-service-tests.md | 63 + ...-client-service-tests_branch-validation.md | 10 + ...t-service-tests_manual-validation-notes.md | 9 + ...nt-service-tests_requirements-checklist.md | 11 + ...pi-client-service-tests_validation-lane.md | 20 + .../dev/reports/codex_changed_files.txt | 65 +- docs_build/dev/reports/codex_review.diff | 2854 +++++++++++++---- package.json | 1 + src/api/session-api-client.js | 2 +- .../ServerApiClientStandardization.test.mjs | 126 + 10 files changed, 2479 insertions(+), 682 deletions(-) create mode 100644 docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md create mode 100644 docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md create mode 100644 docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md create mode 100644 docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md create mode 100644 docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md new file mode 100644 index 000000000..f7d6337e0 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md @@ -0,0 +1,63 @@ +# PR_26175_DELTA_007-runtime-api-client-service-tests + +## Summary + +Team Delta added service-level API client coverage for the standardized Browser -> Server API -> Data Source boundary. + +This PR fixes the session logout API client to use the same `requireServerApiData(...)` unwrap path as the other session client calls. It also adds targeted tests proving logout uses the configured server API URL, returns `payload.data`, and preserves the session-specific restore guidance when server data is missing. + +## Scope + +- Team: Delta +- Branch: `PR_26175_DELTA_007-runtime-api-client-service-tests` +- Runtime file changed: `src/api/session-api-client.js` +- Test file changed: `tests/dev-runtime/ServerApiClientStandardization.test.mjs` +- Service test command added: `npm run test:service:api-client` +- Site-wide command preserved: `npm test` + +## Runtime Impact + +PASS - Session logout now follows the standardized server-data boundary instead of calling an undefined helper. + +## API / Data Impact + +PASS - No API routes or persisted data contracts changed. The browser API client still calls `/session/logout` with `POST` and reads server-owned `payload.data`. + +## Requirement Checklist + +| Requirement | Status | Notes | +|---|---|---| +| One PR purpose only | PASS | API client service testability only. | +| Team Delta ownership only | PASS | API clients and runtime test coverage are Delta-owned. | +| No team-specific test runner | PASS | No Delta-named runner or command added. | +| No `scripts/run-delta-runtime-validation.mjs` | PASS | File was not added. | +| No `test:delta-runtime` | PASS | Script was not added. | +| Testing organized by service/page level | PASS | Added `test:service:api-client`. | +| Keep `npm test` as site-wide command | PASS | Existing `npm test` is unchanged. | +| No UI changes | PASS | No UI files changed. | +| No browser-owned product data | PASS | Test stubs only the server API response; no persisted browser data source added. | +| No silent fallbacks or hidden defaults | PASS | Missing server data still throws explicit restore guidance. | + +## Validation Lane Report + +| Command | Status | Notes | +|---|---|---| +| `node --check src/api/session-api-client.js` | PASS | API client syntax valid. | +| `node --check tests/dev-runtime/ServerApiClientStandardization.test.mjs` | PASS | Test syntax valid. | +| `npm run test:service:api-client` | PASS | 1 targeted file, 6 tests passed. | +| `git diff --check` | PASS | No whitespace errors. | +| `npm run codex:review-artifacts` | PASS | Regenerated `codex_review.diff` and `codex_changed_files.txt`. | + +## Manual Validation Notes + +- Confirmed `package.json` keeps `npm test` unchanged. +- Confirmed no Team Delta-specific validation command was introduced. +- Confirmed session logout test uses mocked server API data instead of browser-owned persisted state. +- Playwright was not run; this is a Node service/API-client coverage change. + +## ZIP + +Expected repo-structured delta ZIP: + +`tmp/PR_26175_DELTA_007-runtime-api-client-service-tests_delta.zip` + diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md new file mode 100644 index 000000000..d8dc22bf1 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md @@ -0,0 +1,10 @@ +# PR_26175_DELTA_007 Branch Validation + +| Check | Status | Evidence | +|---|---|---| +| Started from `main` | PASS | `main` was checked out and fast-forward pulled before work began. | +| Local/origin sync before branch | PASS | `git rev-list --left-right --count main...origin/main` returned `0 0`. | +| Worktree clean before branch | PASS | `git status --short` returned no entries. | +| Working branch | PASS | `PR_26175_DELTA_007-runtime-api-client-service-tests`. | +| Direct commit to `main` avoided | PASS | Changes were made only on the PR branch. | + diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md new file mode 100644 index 000000000..13bd9f8b5 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md @@ -0,0 +1,9 @@ +# PR_26175_DELTA_007 Manual Validation Notes + +- Reviewed changed files for API-client-only scope. +- Confirmed no UI files changed. +- Confirmed no persisted browser-owned product data was introduced. +- Confirmed no `scripts/run-delta-runtime-validation.mjs` file was added. +- Confirmed no `test:delta-runtime` script was added. +- Confirmed `npm test` remains the site-wide/all-tests command. + diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md new file mode 100644 index 000000000..07cd3a160 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md @@ -0,0 +1,11 @@ +# PR_26175_DELTA_007 Requirements Checklist + +| Requirement | Status | Notes | +|---|---|---| +| Branch from updated main | PASS | Branch created after fast-forward pull and sync check. | +| Build smallest scoped change | PASS | Fixed session logout unwrap and added focused API client tests. | +| Validate targeted tests | PASS | `npm run test:service:api-client` passed. | +| Required reports created | PASS | PR report, branch validation, checklist, validation lane, manual notes, review diff, changed files. | +| Repo-structured ZIP created | PASS | `tmp/PR_26175_DELTA_007-runtime-api-client-service-tests_delta.zip`. | +| Open PR | PENDING | Opened after commit and push. | +| Stop before merge approval | PASS | This PR is draft/open only; no merge performed. | diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md new file mode 100644 index 000000000..f3844a691 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md @@ -0,0 +1,20 @@ +# PR_26175_DELTA_007 Validation Lane + +## Targeted Lane + +`npm run test:service:api-client` + +## Results + +| Command | Result | +|---|---| +| `node --check src/api/session-api-client.js` | PASS | +| `node --check tests/dev-runtime/ServerApiClientStandardization.test.mjs` | PASS | +| `npm run test:service:api-client` | PASS | +| `git diff --check` | PASS | + +## Notes + +- The service lane is page/service-level and not named for Team Delta. +- Full `npm test` was not run because this PR is scoped to API client service coverage. + diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index ae2d25d52..c906c0200 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,45 +1,26 @@ -# git diff --name-only pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -- -admin/system-health.html -assets/theme-v2/js/admin-system-health.js -docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md -docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md -docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md -docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md -docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md -docs_build/dev/reports/codex_changed_files.txt -docs_build/dev/reports/codex_review.diff -docs_build/dev/reports/coverage_changed_js_guardrail.txt -docs_build/dev/reports/playwright_v8_coverage_report.txt -src/api/admin-system-health-api-client.js -src/dev-runtime/server/local-api-router.mjs -tests/api/admin-system-health/contract.test.mjs -tests/dev-runtime/AdminHealthOperations.test.mjs -tests/playwright/tools/AdminHealthOperationsPage.spec.mjs - # git status --short - M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md - M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md - M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md - M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md - M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md +M docs_build/dev/reports/codex_changed_files.txt M docs_build/dev/reports/codex_review.diff - M docs_build/dev/reports/playwright_v8_coverage_report.txt + M package.json + M src/api/session-api-client.js + M tests/dev-runtime/ServerApiClientStandardization.test.mjs +?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md +?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md +?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md +?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md +?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md + +# git ls-files --others --exclude-standard +docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md +docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md +docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md +docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md +docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md -# git diff --stat pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -- - admin/system-health.html | 13 +- - assets/theme-v2/js/admin-system-health.js | 41 +- - ...IE_030-r2-storage-health-expanded-validation.md | 35 + - ...health-expanded-validation_branch-validation.md | 17 + - ...-expanded-validation_manual-validation-notes.md | 9 + - ...h-expanded-validation_requirements-checklist.md | 13 + - ...e-health-expanded-validation_validation-lane.md | 22 + - docs_build/dev/reports/codex_changed_files.txt | 70 +- - docs_build/dev/reports/codex_review.diff | 988 +++++++++++---------- - .../dev/reports/coverage_changed_js_guardrail.txt | 6 +- - .../dev/reports/playwright_v8_coverage_report.txt | 17 +- - src/api/admin-system-health-api-client.js | 4 + - src/dev-runtime/server/local-api-router.mjs | 105 ++- - tests/api/admin-system-health/contract.test.mjs | 19 + - tests/dev-runtime/AdminHealthOperations.test.mjs | 6 + - .../tools/AdminHealthOperationsPage.spec.mjs | 8 +- - 16 files changed, 851 insertions(+), 522 deletions(-) +# git diff --stat +docs_build/dev/reports/codex_changed_files.txt | 65 +- + docs_build/dev/reports/codex_review.diff | 1825 +++++++++++++------- + package.json | 1 + + src/api/session-api-client.js | 2 +- + .../ServerApiClientStandardization.test.mjs | 126 ++ + 5 files changed, 1335 insertions(+), 684 deletions(-) \ No newline at end of file diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 8ce24fdc9..c4d0c139e 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,662 +1,2238 @@ -diff --git a/admin/system-health.html b/admin/system-health.html -index 5aa9fa80a..b62f232d7 100644 ---- a/admin/system-health.html -+++ b/admin/system-health.html -@@ -316,16 +316,17 @@ - - Check - Target -+ Timing - Status - - - -- Bucket connectivityLoadingPENDING -- ListLoadingPENDING -- UploadLoadingPENDING -- ReadLoadingPENDING -- DeleteLoadingPENDING -- Last checkedLoadingPENDING -+ Bucket connectivityLoadingLoadingPENDING -+ ListLoadingLoadingPENDING -+ UploadLoadingLoadingPENDING -+ ReadLoadingLoadingPENDING -+ DeleteLoadingLoadingPENDING -+ Last checkedLoadingLoadingPENDING - - - -diff --git a/assets/theme-v2/js/admin-system-health.js b/assets/theme-v2/js/admin-system-health.js -index 746d47753..21bc50a63 100644 ---- a/assets/theme-v2/js/admin-system-health.js -+++ b/assets/theme-v2/js/admin-system-health.js -@@ -1,7 +1,7 @@ - import { - readAdminSystemHealthStatus, - runAdminSystemHealthAction, -- runAdminSystemHealthStorageConnectivityAction, -+ runAdminSystemHealthStorageExpandedValidation, - } from "../../../src/api/admin-system-health-api-client.js"; - import { - applyStatusNode, -@@ -54,6 +54,10 @@ class AdminSystemHealthController { - node.dataset.adminSystemHealthStorageStatus, - node, - ])); -+ this.storageTimings = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-storage-timing]")).map((node) => [ -+ node.dataset.adminSystemHealthStorageTiming, -+ node, -+ ])); - this.runtimeHealthValues = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-runtime-health-value]")).map((node) => [ - node.dataset.adminSystemHealthRuntimeHealthValue, - node, -@@ -122,6 +126,13 @@ class AdminSystemHealthController { - this.setStatusNode(node, status, reason); - } +diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt +index ae2d25d52..eea7be37e 100644 +--- a/docs_build/dev/reports/codex_changed_files.txt ++++ b/docs_build/dev/reports/codex_changed_files.txt +@@ -1,45 +1,26 @@ +-# git diff --name-only pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -- +-admin/system-health.html +-assets/theme-v2/js/admin-system-health.js +-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md +-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md +-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md +-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md +-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md +-docs_build/dev/reports/codex_changed_files.txt +-docs_build/dev/reports/codex_review.diff +-docs_build/dev/reports/coverage_changed_js_guardrail.txt +-docs_build/dev/reports/playwright_v8_coverage_report.txt +-src/api/admin-system-health-api-client.js +-src/dev-runtime/server/local-api-router.mjs +-tests/api/admin-system-health/contract.test.mjs +-tests/dev-runtime/AdminHealthOperations.test.mjs +-tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +- + # git status --short +- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md +- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md +- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md +- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md +- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md ++M docs_build/dev/reports/codex_changed_files.txt + M docs_build/dev/reports/codex_review.diff +- M docs_build/dev/reports/playwright_v8_coverage_report.txt ++ M package.json ++ M src/api/session-api-client.js ++ M tests/dev-runtime/ServerApiClientStandardization.test.mjs ++?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md ++?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md ++?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md ++?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md ++?? docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md ++ ++# git ls-files --others --exclude-standard ++docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md ++docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md ++docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md ++docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md ++docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md -+ setStorageTiming(key, value, fallback) { -+ const node = this.storageTimings.get(key); -+ if (node) { -+ node.textContent = asText(value, fallback); -+ } -+ } +-# git diff --stat pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -- +- admin/system-health.html | 13 +- +- assets/theme-v2/js/admin-system-health.js | 41 +- +- ...IE_030-r2-storage-health-expanded-validation.md | 35 + +- ...health-expanded-validation_branch-validation.md | 17 + +- ...-expanded-validation_manual-validation-notes.md | 9 + +- ...h-expanded-validation_requirements-checklist.md | 13 + +- ...e-health-expanded-validation_validation-lane.md | 22 + +- docs_build/dev/reports/codex_changed_files.txt | 70 +- +- docs_build/dev/reports/codex_review.diff | 988 +++++++++++---------- +- .../dev/reports/coverage_changed_js_guardrail.txt | 6 +- +- .../dev/reports/playwright_v8_coverage_report.txt | 17 +- +- src/api/admin-system-health-api-client.js | 4 + +- src/dev-runtime/server/local-api-router.mjs | 105 ++- +- tests/api/admin-system-health/contract.test.mjs | 19 + +- tests/dev-runtime/AdminHealthOperations.test.mjs | 6 + +- .../tools/AdminHealthOperationsPage.spec.mjs | 8 +- +- 16 files changed, 851 insertions(+), 522 deletions(-) ++# git diff --stat ++docs_build/dev/reports/codex_changed_files.txt | 54 +- ++ docs_build/dev/reports/codex_review.diff | 816 ++++----------------- ++ package.json | 1 + ++ src/api/session-api-client.js | 2 +- ++ .../ServerApiClientStandardization.test.mjs | 126 ++++ ++ 5 files changed, 301 insertions(+), 698 deletions(-) +\ No newline at end of file +diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff +index 8ce24fdc9..4ac91671e 100644 +--- a/docs_build/dev/reports/codex_review.diff ++++ b/docs_build/dev/reports/codex_review.diff +@@ -1,662 +1,1205 @@ +-diff --git a/admin/system-health.html b/admin/system-health.html +-index 5aa9fa80a..b62f232d7 100644 +---- a/admin/system-health.html +-+++ b/admin/system-health.html +-@@ -316,16 +316,17 @@ +- +- Check +- Target +-+ Timing +- Status +- +- +- +-- Bucket connectivityLoadingPENDING +-- ListLoadingPENDING +-- UploadLoadingPENDING +-- ReadLoadingPENDING +-- DeleteLoadingPENDING +-- Last checkedLoadingPENDING +-+ Bucket connectivityLoadingLoadingPENDING +-+ ListLoadingLoadingPENDING +-+ UploadLoadingLoadingPENDING +-+ ReadLoadingLoadingPENDING +-+ DeleteLoadingLoadingPENDING +-+ Last checkedLoadingLoadingPENDING +- +- +- +-diff --git a/assets/theme-v2/js/admin-system-health.js b/assets/theme-v2/js/admin-system-health.js +-index 746d47753..21bc50a63 100644 +---- a/assets/theme-v2/js/admin-system-health.js +-+++ b/assets/theme-v2/js/admin-system-health.js +-@@ -1,7 +1,7 @@ +- import { +- readAdminSystemHealthStatus, +- runAdminSystemHealthAction, +-- runAdminSystemHealthStorageConnectivityAction, +-+ runAdminSystemHealthStorageExpandedValidation, +- } from "../../../src/api/admin-system-health-api-client.js"; ++diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt ++index ae2d25d52..9a155cc2b 100644 ++--- a/docs_build/dev/reports/codex_changed_files.txt +++++ b/docs_build/dev/reports/codex_changed_files.txt ++@@ -1,45 +1,13 @@ ++-# git diff --name-only pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -- ++-admin/system-health.html ++-assets/theme-v2/js/admin-system-health.js ++-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md ++-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md ++-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md ++-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md ++-docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md ++-docs_build/dev/reports/codex_changed_files.txt ++-docs_build/dev/reports/codex_review.diff ++-docs_build/dev/reports/coverage_changed_js_guardrail.txt ++-docs_build/dev/reports/playwright_v8_coverage_report.txt ++-src/api/admin-system-health-api-client.js ++-src/dev-runtime/server/local-api-router.mjs ++-tests/api/admin-system-health/contract.test.mjs ++-tests/dev-runtime/AdminHealthOperations.test.mjs ++-tests/playwright/tools/AdminHealthOperationsPage.spec.mjs ++- ++ # git status --short ++- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md ++- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md ++- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md ++- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md ++- M docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md ++- M docs_build/dev/reports/codex_review.diff ++- M docs_build/dev/reports/playwright_v8_coverage_report.txt +++M package.json +++ M src/api/session-api-client.js +++ M tests/dev-runtime/ServerApiClientStandardization.test.mjs +++ +++# git ls-files --others --exclude-standard +++(no output) ++ ++-# git diff --stat pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -- ++- admin/system-health.html | 13 +- ++- assets/theme-v2/js/admin-system-health.js | 41 +- ++- ...IE_030-r2-storage-health-expanded-validation.md | 35 + ++- ...health-expanded-validation_branch-validation.md | 17 + ++- ...-expanded-validation_manual-validation-notes.md | 9 + ++- ...h-expanded-validation_requirements-checklist.md | 13 + ++- ...e-health-expanded-validation_validation-lane.md | 22 + ++- docs_build/dev/reports/codex_changed_files.txt | 70 +- ++- docs_build/dev/reports/codex_review.diff | 988 +++++++++++---------- ++- .../dev/reports/coverage_changed_js_guardrail.txt | 6 +- ++- .../dev/reports/playwright_v8_coverage_report.txt | 17 +- ++- src/api/admin-system-health-api-client.js | 4 + ++- src/dev-runtime/server/local-api-router.mjs | 105 ++- ++- tests/api/admin-system-health/contract.test.mjs | 19 + ++- tests/dev-runtime/AdminHealthOperations.test.mjs | 6 + ++- .../tools/AdminHealthOperationsPage.spec.mjs | 8 +- ++- 16 files changed, 851 insertions(+), 522 deletions(-) +++# git diff --stat +++package.json | 1 + +++ src/api/session-api-client.js | 2 +- +++ .../ServerApiClientStandardization.test.mjs | 126 +++++++++++++++++++++ +++ 3 files changed, 128 insertions(+), 1 deletion(-) ++\ No newline at end of file ++diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff ++index 8ce24fdc9..6bd489213 100644 ++--- a/docs_build/dev/reports/codex_review.diff +++++ b/docs_build/dev/reports/codex_review.diff ++@@ -1,662 +1,170 @@ ++-diff --git a/admin/system-health.html b/admin/system-health.html ++-index 5aa9fa80a..b62f232d7 100644 ++---- a/admin/system-health.html ++-+++ b/admin/system-health.html ++-@@ -316,16 +316,17 @@ ++- ++- Check ++- Target ++-+ Timing ++- Status ++- ++- ++- ++-- Bucket connectivityLoadingPENDING ++-- ListLoadingPENDING ++-- UploadLoadingPENDING ++-- ReadLoadingPENDING ++-- DeleteLoadingPENDING ++-- Last checkedLoadingPENDING ++-+ Bucket connectivityLoadingLoadingPENDING ++-+ ListLoadingLoadingPENDING ++-+ UploadLoadingLoadingPENDING ++-+ ReadLoadingLoadingPENDING ++-+ DeleteLoadingLoadingPENDING ++-+ Last checkedLoadingLoadingPENDING ++- ++- ++- ++-diff --git a/assets/theme-v2/js/admin-system-health.js b/assets/theme-v2/js/admin-system-health.js ++-index 746d47753..21bc50a63 100644 ++---- a/assets/theme-v2/js/admin-system-health.js ++-+++ b/assets/theme-v2/js/admin-system-health.js ++-@@ -1,7 +1,7 @@ ++- import { ++- readAdminSystemHealthStatus, ++- runAdminSystemHealthAction, ++-- runAdminSystemHealthStorageConnectivityAction, ++-+ runAdminSystemHealthStorageExpandedValidation, ++- } from "../../../src/api/admin-system-health-api-client.js"; +++diff --git a/package.json b/package.json +++index 72c3c9c32..05118574a 100644 +++--- a/package.json ++++++ b/package.json +++@@ -3,6 +3,7 @@ +++ "scripts": { +++ "pretest": "node docs_build/dev/toolbox/checkSharedExtractionGuard.mjs", +++ "test": "node ./scripts/run-node-tests.mjs", ++++ "test:service:api-client": "node ./scripts/run-node-test-files.mjs tests/dev-runtime/ServerApiClientStandardization.test.mjs", +++ "test:audit:locations": "node ./scripts/audit-playwright-test-locations.mjs", +++ "test:playwright:structure": "node ./scripts/audit-playwright-test-locations.mjs", +++ "test:playwright:zero-browser": "node ./scripts/run-targeted-test-lanes.mjs --zero-browser-only", +++diff --git a/src/api/session-api-client.js b/src/api/session-api-client.js +++index f31004ea1..3b8c69125 100644 +++--- a/src/api/session-api-client.js ++++++ b/src/api/session-api-client.js +++@@ -51,7 +51,7 @@ export function signIn(options = {}) { +++ } +++ +++ export function logoutSessionUser() { +++- return unwrap(safeRequestServerApi("/session/logout", { method: "POST" }), "Session logout"); ++++ return requireSessionApiData(safeRequestServerApi("/session/logout", { method: "POST" }), "Session logout"); +++ } +++ +++ export function signOut() { +++diff --git a/tests/dev-runtime/ServerApiClientStandardization.test.mjs b/tests/dev-runtime/ServerApiClientStandardization.test.mjs +++index 5530e4074..a694fa78e 100644 +++--- a/tests/dev-runtime/ServerApiClientStandardization.test.mjs ++++++ b/tests/dev-runtime/ServerApiClientStandardization.test.mjs +++@@ -1,10 +1,82 @@ +++ import assert from "node:assert/strict"; +++ import test from "node:test"; +++ ++++import { clearPublicConfigCache } from "../../src/api/public-config-client.js"; ++ import { ++- applyStatusNode, ++-@@ -54,6 +54,10 @@ class AdminSystemHealthController { ++- node.dataset.adminSystemHealthStorageStatus, ++- node, ++- ])); ++-+ this.storageTimings = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-storage-timing]")).map((node) => [ ++-+ node.dataset.adminSystemHealthStorageTiming, ++-+ node, ++-+ ])); ++- this.runtimeHealthValues = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-runtime-health-value]")).map((node) => [ ++- node.dataset.adminSystemHealthRuntimeHealthValue, ++- node, ++-@@ -122,6 +126,13 @@ class AdminSystemHealthController { ++- this.setStatusNode(node, status, reason); ++- } ++- ++-+ setStorageTiming(key, value, fallback) { ++-+ const node = this.storageTimings.get(key); ++-+ if (node) { ++-+ node.textContent = asText(value, fallback); ++-+ } ++++ clearServerApiDiagnostics, +++ requireServerApiData, +++ SERVER_DATA_BOUNDARY_RULE, +++ } from "../../src/api/server-api-client.js"; ++++import { logoutSessionUser } from "../../src/api/session-api-client.js"; ++++ ++++function installMockServerApi(routes) { ++++ const calls = []; ++++ const previousWindow = globalThis.window; ++++ const previousXmlHttpRequest = globalThis.XMLHttpRequest; ++++ ++++ clearPublicConfigCache(); ++++ clearServerApiDiagnostics(); ++++ ++++ globalThis.window = { ++++ GameFoundryPublicConfig: { ++++ apiUrl: "http://runtime-api.test/api", ++++ }, ++++ }; ++++ ++++ globalThis.XMLHttpRequest = class MockXMLHttpRequest { ++++ constructor() { ++++ this.headers = {}; ++++ this.responseText = ""; ++++ this.status = 0; ++ + } ++ + ++- setRuntimeHealthValue(key, value, fallback) { ++- const node = this.runtimeHealthValues.get(key); ++- if (node) { ++-@@ -195,6 +206,7 @@ class AdminSystemHealthController { ++- renderStoragePending(reason) { ++- ["bucket", "list", "upload", "read", "delete", "lastChecked"].forEach((key) => { ++- this.setStorageStatus(key, "PENDING", reason); ++-+ this.setStorageTiming(key, "not available"); ++- }); ++- this.renderRuntimePending(reason); ++- } ++-@@ -261,8 +273,10 @@ class AdminSystemHealthController { ++- const reason = storageStatus.message || "Cloudflare R2 configuration status returned by the safe Admin System Health API."; ++- this.setStorageValue("bucket", storageStatus.environmentFolder ? `${storageStatus.bucket || "not configured"} ${storageStatus.environmentFolder}` : storageStatus.bucket, "not configured"); ++- this.setStorageStatus("bucket", storageStatus.bucketStatus || storageStatus.status, reason); ++-+ this.setStorageTiming("bucket", storageStatus.lastChecked ? "configuration status" : "not available"); ++- this.setStorageValue("lastChecked", storageStatus.lastChecked, "not available"); ++- this.setStorageStatus("lastChecked", storageStatus.lastChecked ? "PASS" : "WARN", reason); ++-+ this.setStorageTiming("lastChecked", storageStatus.lastChecked ? "configuration status" : "not available"); ++- } ++- ++- renderRuntimeHealthPending(reason) { ++-@@ -684,9 +698,11 @@ class AdminSystemHealthController { ++- } ++- this.setStorageValue(key, this.storageResultTarget(result)); ++- this.setStorageStatus(key, result.status, result.message || "R2 diagnostic returned without a message."); ++-+ this.setStorageTiming(key, Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); ++- if (result.lastChecked) { ++- this.setStorageValue("lastChecked", result.lastChecked); ++- this.setStorageStatus("lastChecked", "PASS", "Most recent current-environment R2 health check timestamp."); ++-+ this.setStorageTiming("lastChecked", Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); ++- } ++- } ++- ++-@@ -732,15 +748,24 @@ class AdminSystemHealthController { ++- runStorageDiagnostics() { ++- STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { ++- this.setStorageStatus(key, "PENDING", "R2 diagnostic is running through the safe Admin System Health API."); ++-+ this.setStorageTiming(key, "running"); ++- }); ++-- STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ actionId, key }) => { ++-- try { ++-- this.renderStorageResult(key, runAdminSystemHealthStorageConnectivityAction(actionId)); ++-- } catch (error) { ++-- const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; ++-+ try { ++-+ const validation = runAdminSystemHealthStorageExpandedValidation(); ++-+ const diagnostics = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; ++-+ diagnostics.forEach((storageResult) => { ++-+ const key = STORAGE_DIAGNOSTIC_ACTION_KEY_BY_ID.get(storageResult.actionId); ++-+ if (key) { ++-+ this.renderStorageResult(key, storageResult); ++-+ } ++-+ }); ++-+ } catch (error) { ++-+ const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; ++-+ STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { ++- this.setStorageStatus(key, "PENDING", message); ++-- } ++-- }); ++-+ this.setStorageTiming(key, "not available"); ++-+ }); ++-+ } ++- } ++- ++- createCell(text) { ++-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md ++-new file mode 100644 ++-index 000000000..61eba9efb ++---- /dev/null ++-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md ++-@@ -0,0 +1,35 @@ ++-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation ++-+ ++-+Team: Charlie ++-+Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation ++-+Base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel ++-+Lifecycle: Build / Validation ++-+Repair: Rebased onto repaired PR_26177_CHARLIE_029 branch on 2026-06-25. ++-+ ++-+## Scope ++-+- Expanded System Health R2 validation into an API-owned current-environment validation run. ++-+- Added safe list/upload/read/delete diagnostics with duration, operation label, cleanup status, and permanent-object metadata. ++-+- Updated the System Health UI to show storage timing without adding inline styles, script blocks, or page-local CSS. ++-+ ++-+## Changed Files ++-+- admin/system-health.html ++-+- assets/theme-v2/js/admin-system-health.js ++-+- src/api/admin-system-health-api-client.js ++-+- src/dev-runtime/server/local-api-router.mjs ++-+- tests/api/admin-system-health/contract.test.mjs ++-+- tests/dev-runtime/AdminHealthOperations.test.mjs ++-+- tests/playwright/tools/AdminHealthOperationsPage.spec.mjs ++-+- docs_build/dev/reports/coverage_changed_js_guardrail.tx ++-+- docs_build/dev/reports/playwright_v8_coverage_report.tx ++-+ ++-+## Validation ++-+- PASS: node --check src/dev-runtime/server/local-api-router.mjs ++-+- PASS: node --check assets/theme-v2/js/admin-system-health.js ++-+- PASS: node --check src/api/admin-system-health-api-client.js ++-+- PASS: node --test tests/api/admin-system-health/contract.test.mjs ++-+- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs ++-+- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line ++-+- PASS: git diff --check ++-+ ++-+## ZIP ++-+- Generated after repair: C:\Users\DavidQ\Documents\GitHub\HTML-JavaScript-Gaming\tmp\PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_delta.zip ++-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md ++-new file mode 100644 ++-index 000000000..0c0387294 ++---- /dev/null ++-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md ++-@@ -0,0 +1,17 @@ ++-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Branch Validation ++-+ ++-+Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation ++-+Expected stack base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel ++-+Current status at validation: ++-+## pr/26177-CHARLIE-030-r2-storage-health-expanded-validation ++-+Updated onto repaired PR_26177_CHARLIE_029 stack base. ++-+ ++-+Result: PASS ++-+ ++-+Checks: ++-+- PASS: Branch created from PR 029 branch for the approved stacked chain. ++-+- PASS: Active branch matches PR identity. ++-+- PASS: Worktree contained only scoped PR changes and generated validation reports. ++-+- PASS: Branch is based on repaired PR 029 branch. ++-+- PASS: Rebase conflict scope was generated report artifacts only. ++-+- PASS: No start_of_day files modified. ++-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md ++-new file mode 100644 ++-index 000000000..36806ff77 ++---- /dev/null ++-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md ++-@@ -0,0 +1,9 @@ ++-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Manual Validation Notes ++-+ ++-+- Confirmed the browser calls one expanded R2 validation action instead of owning the list/upload/read/delete sequence itself. ++-+- Confirmed Storage Health displays timing values for list, upload, read, and delete rows. ++-+- Confirmed storage validation payload includes cleanup status and permanent-object metadata. ++-+- Confirmed storage diagnostics stay scoped to the current environment folder. ++-+- Confirmed no storage credentials or secrets are displayed. ++-+- Confirmed branch repair conflict was limited to generated report artifacts. ++-+- Confirmed no start_of_day files changed. ++-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md ++-new file mode 100644 ++-index 000000000..2dda36cc8 ++---- /dev/null ++-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md ++-@@ -0,0 +1,13 @@ ++-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Requirement Checklis ++-+ ++-+- PASS: Expanded Cloud/R2 storage health validation. ++-+- PASS: Included safe bucket/list/write/read/delete validation through API-owned action contract. ++-+- PASS: Included timing/status fields where available. ++-+- PASS: Does not create browser-owned storage health state. ++-+- PASS: Test object cleanup is attempted in the same expanded validation run. ++-+- PASS: Does not expose secrets. ++-+- PASS: Current environment folder only. ++-+- PASS: No unrelated files modified. ++-+- PASS: No start_of_day files modified. ++-+- PASS: Rebased onto repaired PR_26177_CHARLIE_029 branch. ++-+- PASS: Shows unavailable/not configured states explicitly. ++-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md ++-new file mode 100644 ++-index 000000000..0dbf85ec0 ++---- /dev/null ++-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md ++-@@ -0,0 +1,22 @@ ++-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Validation Lane Repor ++-+ ++-+Impacted lanes: ++-+- runtime: Local API Admin System Health R2 action contract. ++-+- contract: Admin System Health API contract tests. ++-+- UI: Admin System Health Theme V2 page controller and markup. ++-+- Playwright: targeted Admin System Health page spec. ++-+ ++-+Commands: ++-+- PASS: node --check src/dev-runtime/server/local-api-router.mjs ++-+- PASS: node --check assets/theme-v2/js/admin-system-health.js ++-+- PASS: node --check src/api/admin-system-health-api-client.js ++-+- PASS: node --test tests/api/admin-system-health/contract.test.mjs ++-+- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs ++-+- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line ++-+- PASS: git diff --check ++-+ ++-+Skipped lanes: ++-+- Full samples smoke skipped; not impacted by System Health storage diagnostics. ++-+- Full Project Workspace suite skipped; targeted Admin/System Health coverage was sufficient for this scoped page/API change. ++-+ ++-+Result: PASS ++-diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.tx ++-index 7b1c51f19..7211f2375 100644 ++---- a/docs_build/dev/reports/coverage_changed_js_guardrail.tx ++-+++ b/docs_build/dev/reports/coverage_changed_js_guardrail.tx ++-@@ -6,7 +6,9 @@ Missing changed runtime JS files are WARN, not FAIL. ++- Source: Playwright/Chromium built-in V8 coverage from the active Playwright run. ++- ++- Changed runtime JS files considered: ++--(100%) none changed - no changed runtime JS files ++-+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only ++-+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 ++-+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 ++- ++- Guardrail warnings: ++--(100%) none changed - no changed runtime JS files ++-+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file missing from coverage; advisory only ++-diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.tx ++-index 627ed631f..2a2982e6a 100644 ++---- a/docs_build/dev/reports/playwright_v8_coverage_report.tx ++-+++ b/docs_build/dev/reports/playwright_v8_coverage_report.tx ++-@@ -17,7 +17,9 @@ Exercised tool entry points detected: ++- (78%) Theme V2 Shared JS - exercised 5 runtime JS files ++- ++- Changed runtime JS files covered: ++--(100%) none changed - no changed runtime JS files ++-+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only ++-+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 ++-+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 ++- ++- Files with executed line/function counts where available: ++- (36%) src/api/server-api-client.js - executed lines 168/168; executed functions 5/14 ++-@@ -27,13 +29,18 @@ Files with executed line/function counts where available: ++- (77%) assets/theme-v2/js/tool-display-mode.js - executed lines 304/304; executed functions 23/30 ++- (80%) assets/theme-v2/js/theme-icons.js - executed lines 69/69; executed functions 4/5 ++- (80%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 4/5 ++--(81%) assets/theme-v2/js/admin-system-health.js - executed lines 848/848; executed functions 70/86 ++-+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 ++- (83%) assets/js/shared/status.js - executed lines 37/37; executed functions 5/6 ++- (91%) assets/theme-v2/js/admin-owner-navigation.js - executed lines 58/58; executed functions 10/11 ++--(100%) src/api/admin-system-health-api-client.js - executed lines 28/28; executed functions 4/4 ++-+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 ++- ++- Uncovered or low-coverage changed JS files: ++--(100%) none changed - no changed runtime JS files ++-+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: uncovered changed runtime JS file; advisory only ++- ++- Changed JS files considered: ++--(100%) none - no changed JS files ++-+(0%) src/dev-runtime/server/local-api-router.mjs - changed JS file not collected as browser runtime coverage ++-+(0%) tests/api/admin-system-health/contract.test.mjs - changed JS file not collected as browser runtime coverage ++-+(0%) tests/dev-runtime/AdminHealthOperations.test.mjs - changed JS file not collected as browser runtime coverage ++-+(0%) tests/playwright/tools/AdminHealthOperationsPage.spec.mjs - changed JS file not collected as browser runtime coverage ++-+(81%) assets/theme-v2/js/admin-system-health.js - changed JS file with browser V8 coverage ++-+(100%) src/api/admin-system-health-api-client.js - changed JS file with browser V8 coverage ++-diff --git a/src/api/admin-system-health-api-client.js b/src/api/admin-system-health-api-client.js ++-index 9b166d097..49101c59f 100644 ++---- a/src/api/admin-system-health-api-client.js ++-+++ b/src/api/admin-system-health-api-client.js ++-@@ -20,6 +20,10 @@ export function runAdminSystemHealthStorageConnectivityAction(actionId) { ++- ); ++- } ++- ++-+export function runAdminSystemHealthStorageExpandedValidation() { ++-+ return runAdminSystemHealthStorageConnectivityAction("storage-expanded-validation"); ++-+} ++-+ ++- export function runAdminSystemHealthAction(actionId) { ++- return requireServerApiData( ++- safeRequestServerApi("/admin/system-health/action", { ++-diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs ++-index 462d8b7a0..6dbb8a106 100644 ++---- a/src/dev-runtime/server/local-api-router.mjs ++-+++ b/src/dev-runtime/server/local-api-router.mjs ++-@@ -356,6 +356,7 @@ const SYSTEM_HEALTH_STORAGE_ACTION_IDS = Object.freeze([ ++- "storage-read-test-object", ++- "storage-delete-test-object", ++- ]); ++-+const SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID = "storage-expanded-validation"; ++- const SYSTEM_HEALTH_MANUAL_ACTION_LABELS = Object.freeze({ ++- "database-check": "Run Database Check", ++- "full-health-check": "Run Full Health Check", ++-@@ -4046,43 +4047,75 @@ class ApiRuntimeDataSource { ++- ++- storageConnectivityConfigFailure(actionId, storage, scope = "project-prefix") { ++- const missing = storage.config?.missingKeys?.join(", ") || storage.config?.validationError || "storage configuration incomplete"; ++-+ const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); ++- return { ++- actionId, ++-+ cleanupStatus: "not-run", ++-+ durationMs: 0, ++- environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", ++- executed: false, ++- lastChecked: new Date().toISOString(), ++- message: `Storage connectivity requires configured storage and GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX before this action can run. Missing or invalid: ${missing}.`, ++-+ operation: action?.operation || "configuration", ++-+ operationLabel: action?.label || "Storage configuration", ++-+ permanentObjectCreated: false, ++- projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), ++- secretEditingAllowed: false, ++- secretsExposed: false, ++- status: "FAIL", ++- storageStatus: this.ownerStorageStatus(), ++-+ targetPrefix: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : String(storage.config?.projectsPrefix || "").trim(), ++- }; ++- } ++- ++-- storageConnectivityResult({ actionId, environmentFolder = "", executed = true, keysListed = 0, message, objectKey, projectsPrefix, status }) { ++-+ storageConnectivityResult({ ++-+ action, ++-+ actionId, ++-+ cleanupStatus = "not-applicable", ++-+ durationMs = 0, ++-+ environmentFolder = "", ++-+ executed = true, ++-+ keysListed = 0, ++-+ message, ++-+ objectKey, ++-+ permanentObjectCreated = false, ++-+ projectsPrefix, ++-+ status, ++-+ targetPrefix = "", ++-+ }) { ++-+ const resolvedAction = action || STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId) || {}; ++- return { ++- actionId, ++-+ cleanupStatus, ++-+ durationMs, ++- environmentFolder, ++- executed, ++- keysListed, ++- lastChecked: new Date().toISOString(), ++- message, ++-+ operation: resolvedAction.operation || "", ++-+ operationLabel: resolvedAction.label || actionId, ++-+ permanentObjectCreated, ++- projectsPrefix, ++- secretEditingAllowed: false, ++- secretsExposed: false, ++- status, ++- storageStatus: this.ownerStorageStatus(), ++-+ targetPrefix, ++- testObjectKey: objectKey, ++- }; ++- } ++- ++- async runStorageConnectivityAction(actionId, options = {}) { ++-+ if (actionId === SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID) { ++-+ return this.runStorageExpandedValidation(options); ++++ open(method, url, async) { ++++ this.method = method; ++++ this.url = url; ++++ this.async = async; ++ + } ++- const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); ++- if (!action) { ++- throw new Error(`Unknown storage connectivity action: ${actionId || "missing actionId"}.`); ++- } ++- ++-+ const startedAt = Date.now(); ++- const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; ++- const storage = createConfiguredProjectAssetStorage(); ++- const projectsPrefix = String(storage.config?.projectsPrefix || "").trim(); ++-@@ -4098,7 +4131,9 @@ class ApiRuntimeDataSource { ++- const result = await storage.listObjects(targetPrefix); ++- const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; ++- return this.storageConnectivityResult({ ++-+ action, ++- actionId: action.id, ++-+ durationMs: Date.now() - startedAt, ++- environmentFolder, ++- keysListed, ++- message: result.ok ++-@@ -4107,6 +4142,7 @@ class ApiRuntimeDataSource { ++- objectKey, ++- projectsPrefix, ++- status: result.ok ? "PASS" : "FAIL", ++-+ targetPrefix, ++- }); ++- } ++- ++-@@ -4116,7 +4152,9 @@ class ApiRuntimeDataSource { ++- : await storage.listProjectObjects(); ++- const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; ++- return this.storageConnectivityResult({ ++-+ action, ++- actionId: action.id, ++-+ durationMs: Date.now() - startedAt, ++- environmentFolder, ++- keysListed, ++- message: result.ok ++-@@ -4125,6 +4163,7 @@ class ApiRuntimeDataSource { ++- objectKey, ++- projectsPrefix, ++- status: result.ok ? "PASS" : "FAIL", ++-+ targetPrefix, ++- }); ++- } ++- ++-@@ -4135,14 +4174,19 @@ class ApiRuntimeDataSource { ++- objectKey, ++- }); ++- return this.storageConnectivityResult({ ++-+ action, ++- actionId: action.id, ++-+ cleanupStatus: result.ok ? "requires-delete-action" : "not-created", ++-+ durationMs: Date.now() - startedAt, ++- environmentFolder, ++- message: result.ok ++- ? `Upload test object completed at ${objectKey}. ++- : `${result.message || "Storage upload failed."} Verify upload permission for ${targetPrefix}.`, ++- objectKey, ++-+ permanentObjectCreated: result.ok === true, ++- projectsPrefix, ++- status: result.ok ? "PASS" : "FAIL", ++-+ targetPrefix, ++- }); ++- } ++- ++-@@ -4151,7 +4195,9 @@ class ApiRuntimeDataSource { ++- const text = result.ok ? Buffer.from(result.bytes || []).toString("utf8") : ""; ++- const contentMatches = text === STORAGE_CONNECTIVITY_TEST_OBJECT_CONTENT; ++- return this.storageConnectivityResult({ ++-+ action, ++- actionId: action.id, ++-+ durationMs: Date.now() - startedAt, ++- environmentFolder, ++- message: result.ok && contentMatches ++- ? `Read test object completed from ${objectKey}. ++-@@ -4159,32 +4205,77 @@ class ApiRuntimeDataSource { ++- objectKey, ++- projectsPrefix, ++- status: result.ok && contentMatches ? "PASS" : "FAIL", ++-+ targetPrefix, ++- }); ++- } ++- ++- const result = await storage.deleteObject(objectKey); ++- return this.storageConnectivityResult({ ++-+ action, ++- actionId: action.id, ++-+ cleanupStatus: result.ok ? "completed" : "failed", ++-+ durationMs: Date.now() - startedAt, ++- environmentFolder, ++- message: result.ok ++- ? `Delete test object completed at ${objectKey}. ++- : `${result.message || "Storage delete failed."} Verify delete permission for ${targetPrefix}.`, ++- objectKey, ++-+ permanentObjectCreated: false, ++- projectsPrefix, ++- status: result.ok ? "PASS" : "FAIL", ++-+ targetPrefix, ++- }); ++- } catch (error) { ++- return this.storageConnectivityResult({ ++-+ action, ++- actionId: action.id, ++-+ durationMs: Date.now() - startedAt, ++- environmentFolder, ++- message: `Storage connectivity action failed: ${error instanceof Error ? error.message : String(error || "Unknown storage error.")}`, ++- objectKey, ++- projectsPrefix, ++- status: "FAIL", ++-+ targetPrefix, ++- }); ++- } ++- } ++- ++-+ async runStorageExpandedValidation(options = {}) { ++-+ const startedAt = Date.now(); ++-+ const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; ++-+ const storage = createConfiguredProjectAssetStorage(); ++-+ const targetPrefix = this.storageConnectivityTargetPrefix(storage, scope); ++-+ const objectKey = this.storageConnectivityTestObjectKey(storage, scope); ++-+ const results = []; ++-+ for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { ++-+ results.push(await this.runStorageConnectivityAction(actionId, { scope })); ++++ ++++ setRequestHeader(name, value) { ++++ this.headers[name] = value; ++ + } ++-+ const uploadResult = results.find((result) => result.actionId === "storage-upload-test-object"); ++-+ const deleteResult = results.find((result) => result.actionId === "storage-delete-test-object"); ++-+ const permanentObjectCreated = uploadResult?.status === "PASS" && deleteResult?.status !== "PASS"; ++-+ const status = overallHealthStatus(results); ++-+ return { ++-+ actionId: SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID, ++-+ cleanupStatus: permanentObjectCreated ? "failed" : "completed", ++-+ durationMs: Date.now() - startedAt, ++-+ environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", ++-+ executed: true, ++-+ lastChecked: new Date().toISOString(), ++-+ message: permanentObjectCreated ++-+ ? `Expanded R2 validation detected a cleanup failure for ${objectKey}; review storage credentials and delete permission. ++-+ : `Expanded R2 validation completed against ${targetPrefix}; test object cleanup was attempted in the same validation run.`, ++-+ permanentObjectCreated, ++-+ projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), ++-+ secretEditingAllowed: false, ++-+ secretsExposed: false, ++-+ status, ++-+ storageDiagnostics: results, ++-+ storageStatus: this.ownerStorageStatus(), ++-+ targetPrefix, ++-+ testObjectKey: objectKey, ++-+ }; ++-+ } ++ + ++- ownerPromotionFoundation() { ++- const steps = [ ++- { ++-@@ -4424,20 +4515,20 @@ SELECT pg_database_size(current_database()) AS database_size_bytes, ++- } ++- ++- async adminSystemHealthStorageHealthCheck() { ++-- const results = []; ++-- for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { ++-- results.push(await this.runStorageConnectivityAction(actionId, { scope: "environment-folder" })); ++-- } ++-+ const validation = await this.runStorageExpandedValidation({ scope: "environment-folder" }); ++-+ const results = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; ++- return { ++- actionId: "storage-check", ++- checkedAt: new Date().toISOString(), ++- label: SYSTEM_HEALTH_MANUAL_ACTION_LABELS["storage-check"], ++-- message: "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", ++-+ message: validation.message || "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", ++-+ permanentObjectCreated: validation.permanentObjectCreated === true, ++- secretEditingAllowed: false, ++- secretsExposed: false, ++-- status: overallHealthStatus(results.map((result) => ({ status: result.status }))), ++-+ status: validation.status || overallHealthStatus(results.map((result) => ({ status: result.status }))), ++- storageDiagnostics: results, ++- storageStatus: this.ownerStorageStatus(), ++-+ validationDurationMs: validation.durationMs, ++- }; ++- } ++- ++-diff --git a/tests/api/admin-system-health/contract.test.mjs b/tests/api/admin-system-health/contract.test.mjs ++-index 25aa84b34..54e821f13 100644 ++---- a/tests/api/admin-system-health/contract.test.mjs ++-+++ b/tests/api/admin-system-health/contract.test.mjs ++-@@ -146,6 +146,25 @@ test("Admin System Health completion contract remains server-owned and current-e ++- assert.equal(healthText.includes("/uat/projects"), false); ++- assert.equal(health.secretEditingAllowed, false); ++- assert.equal(health.secretsExposed, false); ++-+ const expandedStorage = await apiJson(server.baseUrl, "/api/admin/system-health/storage-connectivity-action", { ++-+ body: { actionId: "storage-expanded-validation" }, ++-+ method: "POST", ++++ send(body) { ++++ calls.push({ ++++ body, ++++ headers: { ...this.headers }, ++++ method: this.method, ++++ url: this.url, ++ + }); ++-+ assert.deepEqual( ++-+ expandedStorage.storageDiagnostics.map((row) => row.actionId), ++-+ [ ++-+ "storage-bucket-connectivity", ++-+ "storage-list", ++-+ "storage-upload-test-object", ++-+ "storage-read-test-object", ++-+ "storage-delete-test-object", ++-+ ], ++-+ ); ++-+ assert.equal(expandedStorage.storageDiagnostics.every((row) => row.environmentFolder === "/dev"), true); ++-+ assert.equal(expandedStorage.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); ++-+ assert.equal(expandedStorage.permanentObjectCreated, false); ++-+ assert.equal(JSON.stringify(expandedStorage).includes("api-secret"), false); ++-+ assert.equal(JSON.stringify(expandedStorage).includes("site-secret"), false); ++- } finally { ++- await server.close(); ++- } ++-diff --git a/tests/dev-runtime/AdminHealthOperations.test.mjs b/tests/dev-runtime/AdminHealthOperations.test.mjs ++-index 99fef0757..9ea40988a 100644 ++---- a/tests/dev-runtime/AdminHealthOperations.test.mjs ++-+++ b/tests/dev-runtime/AdminHealthOperations.test.mjs ++-@@ -311,6 +311,12 @@ test("Admin can view operational health while Creator sessions are blocked", asy ++- ], ++- ); ++- assert.equal(storageAction.storageDiagnostics.every((row) => row.environmentFolder === "/local"), true); ++-+ assert.equal(storageAction.permanentObjectCreated, false); ++-+ assert.equal(typeof storageAction.validationDurationMs, "number"); ++-+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); ++-+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.operationLabel === "string"), true); ++-+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.cleanupStatus === "string"), true); ++-+ assert.equal(storageAction.storageDiagnostics.every((row) => row.permanentObjectCreated === false), true); ++- const refreshAction = await apiJson(server.baseUrl, "/api/admin/system-health/action", { ++- body: { actionId: "refresh" }, ++- method: "POST", ++-diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs ++-index 56733ce6a..713344ac0 100644 ++---- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs ++-+++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs ++-@@ -233,6 +233,10 @@ test("Admin System Health renders Postgres diagnostics through the safe status A ++- await expect(page.locator("[data-admin-system-health-storage-value='upload']")).toContainText("/dev"); ++- await expect(page.locator("[data-admin-system-health-storage-value='read']")).not.toHaveText("Health object"); ++- await expect(page.locator("[data-admin-system-health-storage-value='delete']")).not.toHaveText("Health object"); ++-+ await expect(page.locator("[data-admin-system-health-storage-timing='list']")).not.toHaveText("Loading"); ++-+ await expect(page.locator("[data-admin-system-health-storage-timing='upload']")).not.toHaveText("Loading"); ++-+ await expect(page.locator("[data-admin-system-health-storage-timing='read']")).not.toHaveText("Loading"); ++-+ await expect(page.locator("[data-admin-system-health-storage-timing='delete']")).not.toHaveText("Loading"); ++- await expect(page.getByRole("table", { name: "Runtime health" })).toContainText("Runtime Health"); ++- await expect(page.locator("[data-admin-system-health-runtime-health-value='environment']")).toHaveText("DEV"); ++- await expect(page.locator("[data-admin-system-health-runtime-health-value='appVersion']")).toHaveText("1.0.0"); ++-@@ -286,7 +290,7 @@ test("Admin System Health renders Postgres diagnostics through the safe status A ++- expect((title || ariaLabel || "").trim()).not.toEqual(""); ++- } ++- expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/status"))).toBe(true); ++-- expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(5); ++-+ expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(1); ++- await page.getByRole("button", { name: "Run Runtime Check" }).click(); ++- await expect(page.getByRole("table", { name: "Manual health action results" })).toContainText("Run Runtime Check"); ++- expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/action"))).toBe(true); ++-@@ -353,5 +357,5 @@ test("Admin System Health operations page keeps scripts and styles external", as ++- expect(runtimeSource).not.toContain("localStorage"); ++- expect(runtimeSource).not.toContain("sessionStorage"); ++- expect(runtimeSource).toContain("runAdminSystemHealthAction"); ++-- expect(runtimeSource).toContain("runAdminSystemHealthStorageConnectivityAction"); ++-+ expect(runtimeSource).toContain("runAdminSystemHealthStorageExpandedValidation"); ++++ const route = routes[`${this.method} ${this.url}`] || { ++++ payload: { ++++ error: `No mock route for ${this.method} ${this.url}`, ++++ ok: false, ++++ }, ++++ status: 404, ++++ }; ++++ this.status = route.status; ++++ this.responseText = JSON.stringify(route.payload); ++++ } ++++ }; ++++ ++++ return { ++++ calls, ++++ restore() { ++++ if (previousWindow === undefined) { ++++ delete globalThis.window; ++++ } else { ++++ globalThis.window = previousWindow; ++++ } ++++ if (previousXmlHttpRequest === undefined) { ++++ delete globalThis.XMLHttpRequest; ++++ } else { ++++ globalThis.XMLHttpRequest = previousXmlHttpRequest; ++++ } ++++ clearPublicConfigCache(); ++++ clearServerApiDiagnostics(); ++++ }, ++++ }; ++++} +++ +++ test("requireServerApiData returns the server data payload", () => { +++ assert.deepEqual( +++@@ -57,3 +129,57 @@ test("requireServerApiData supports client-specific restore guidance", () => { +++ /Admin setup status did not return server data\. Restore the admin setup API\./, +++ ); ++ }); ++++ ++++test("logoutSessionUser uses the standardized session server data boundary", () => { ++++ const mockApi = installMockServerApi({ ++++ "POST http://runtime-api.test/api/session/logout": { ++++ payload: { ++++ data: { ++++ authenticated: false, ++++ userKey: "", ++++ }, ++++ ok: true, ++++ }, ++++ status: 200, ++++ }, ++++ }); ++++ ++++ try { ++++ assert.deepEqual(logoutSessionUser(), { ++++ authenticated: false, ++++ userKey: "", ++++ }); ++++ assert.deepEqual(mockApi.calls, [ ++++ { ++++ body: null, ++++ headers: { ++++ Accept: "application/json", ++++ }, ++++ method: "POST", ++++ url: "http://runtime-api.test/api/session/logout", ++++ }, ++++ ]); ++++ } finally { ++++ mockApi.restore(); ++++ } ++++}); ++++ ++++test("logoutSessionUser preserves session restore guidance for missing server data", () => { ++++ const mockApi = installMockServerApi({ ++++ "POST http://runtime-api.test/api/session/logout": { ++++ payload: { ++++ ok: true, ++++ }, ++++ status: 200, ++++ }, ++++ }); ++++ ++++ try { ++++ assert.throws( ++++ () => logoutSessionUser(), ++++ /Session logout did not return server data\. Restore the server auth\/session API\./, ++++ ); ++++ } finally { ++++ mockApi.restore(); ++++ } ++++}); ++diff --git a/package.json b/package.json ++index 72c3c9c32..05118574a 100644 ++--- a/package.json +++++ b/package.json ++@@ -3,6 +3,7 @@ ++ "scripts": { ++ "pretest": "node docs_build/dev/toolbox/checkSharedExtractionGuard.mjs", ++ "test": "node ./scripts/run-node-tests.mjs", +++ "test:service:api-client": "node ./scripts/run-node-test-files.mjs tests/dev-runtime/ServerApiClientStandardization.test.mjs", ++ "test:audit:locations": "node ./scripts/audit-playwright-test-locations.mjs", ++ "test:playwright:structure": "node ./scripts/audit-playwright-test-locations.mjs", ++ "test:playwright:zero-browser": "node ./scripts/run-targeted-test-lanes.mjs --zero-browser-only", ++diff --git a/src/api/session-api-client.js b/src/api/session-api-client.js ++index f31004ea1..3b8c69125 100644 ++--- a/src/api/session-api-client.js +++++ b/src/api/session-api-client.js ++@@ -51,7 +51,7 @@ export function signIn(options = {}) { ++ } ++ ++ export function logoutSessionUser() { ++- return unwrap(safeRequestServerApi("/session/logout", { method: "POST" }), "Session logout"); +++ return requireSessionApiData(safeRequestServerApi("/session/logout", { method: "POST" }), "Session logout"); ++ } + - setRuntimeHealthValue(key, value, fallback) { - const node = this.runtimeHealthValues.get(key); - if (node) { -@@ -195,6 +206,7 @@ class AdminSystemHealthController { - renderStoragePending(reason) { - ["bucket", "list", "upload", "read", "delete", "lastChecked"].forEach((key) => { - this.setStorageStatus(key, "PENDING", reason); -+ this.setStorageTiming(key, "not available"); - }); - this.renderRuntimePending(reason); - } -@@ -261,8 +273,10 @@ class AdminSystemHealthController { - const reason = storageStatus.message || "Cloudflare R2 configuration status returned by the safe Admin System Health API."; - this.setStorageValue("bucket", storageStatus.environmentFolder ? `${storageStatus.bucket || "not configured"} ${storageStatus.environmentFolder}` : storageStatus.bucket, "not configured"); - this.setStorageStatus("bucket", storageStatus.bucketStatus || storageStatus.status, reason); -+ this.setStorageTiming("bucket", storageStatus.lastChecked ? "configuration status" : "not available"); - this.setStorageValue("lastChecked", storageStatus.lastChecked, "not available"); - this.setStorageStatus("lastChecked", storageStatus.lastChecked ? "PASS" : "WARN", reason); -+ this.setStorageTiming("lastChecked", storageStatus.lastChecked ? "configuration status" : "not available"); - } ++ export function signOut() { ++diff --git a/tests/dev-runtime/ServerApiClientStandardization.test.mjs b/tests/dev-runtime/ServerApiClientStandardization.test.mjs ++index 5530e4074..a694fa78e 100644 ++--- a/tests/dev-runtime/ServerApiClientStandardization.test.mjs +++++ b/tests/dev-runtime/ServerApiClientStandardization.test.mjs ++@@ -1,10 +1,82 @@ ++ import assert from "node:assert/strict"; ++ import test from "node:test"; ++ +++import { clearPublicConfigCache } from "../../src/api/public-config-client.js"; + import { +- applyStatusNode, +-@@ -54,6 +54,10 @@ class AdminSystemHealthController { +- node.dataset.adminSystemHealthStorageStatus, +- node, +- ])); +-+ this.storageTimings = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-storage-timing]")).map((node) => [ +-+ node.dataset.adminSystemHealthStorageTiming, +-+ node, +-+ ])); +- this.runtimeHealthValues = new Map(Array.from(root.querySelectorAll("[data-admin-system-health-runtime-health-value]")).map((node) => [ +- node.dataset.adminSystemHealthRuntimeHealthValue, +- node, +-@@ -122,6 +126,13 @@ class AdminSystemHealthController { +- this.setStatusNode(node, status, reason); +- } +- +-+ setStorageTiming(key, value, fallback) { +-+ const node = this.storageTimings.get(key); +-+ if (node) { +-+ node.textContent = asText(value, fallback); +-+ } +++ clearServerApiDiagnostics, ++ requireServerApiData, ++ SERVER_DATA_BOUNDARY_RULE, ++ } from "../../src/api/server-api-client.js"; +++import { logoutSessionUser } from "../../src/api/session-api-client.js"; +++ +++function installMockServerApi(routes) { +++ const calls = []; +++ const previousWindow = globalThis.window; +++ const previousXmlHttpRequest = globalThis.XMLHttpRequest; +++ +++ clearPublicConfigCache(); +++ clearServerApiDiagnostics(); +++ +++ globalThis.window = { +++ GameFoundryPublicConfig: { +++ apiUrl: "http://runtime-api.test/api", +++ }, +++ }; +++ +++ globalThis.XMLHttpRequest = class MockXMLHttpRequest { +++ constructor() { +++ this.headers = {}; +++ this.responseText = ""; +++ this.status = 0; + + } + + +- setRuntimeHealthValue(key, value, fallback) { +- const node = this.runtimeHealthValues.get(key); +- if (node) { +-@@ -195,6 +206,7 @@ class AdminSystemHealthController { +- renderStoragePending(reason) { +- ["bucket", "list", "upload", "read", "delete", "lastChecked"].forEach((key) => { +- this.setStorageStatus(key, "PENDING", reason); +-+ this.setStorageTiming(key, "not available"); +- }); +- this.renderRuntimePending(reason); +- } +-@@ -261,8 +273,10 @@ class AdminSystemHealthController { +- const reason = storageStatus.message || "Cloudflare R2 configuration status returned by the safe Admin System Health API."; +- this.setStorageValue("bucket", storageStatus.environmentFolder ? `${storageStatus.bucket || "not configured"} ${storageStatus.environmentFolder}` : storageStatus.bucket, "not configured"); +- this.setStorageStatus("bucket", storageStatus.bucketStatus || storageStatus.status, reason); +-+ this.setStorageTiming("bucket", storageStatus.lastChecked ? "configuration status" : "not available"); +- this.setStorageValue("lastChecked", storageStatus.lastChecked, "not available"); +- this.setStorageStatus("lastChecked", storageStatus.lastChecked ? "PASS" : "WARN", reason); +-+ this.setStorageTiming("lastChecked", storageStatus.lastChecked ? "configuration status" : "not available"); +- } +- +- renderRuntimeHealthPending(reason) { +-@@ -684,9 +698,11 @@ class AdminSystemHealthController { +- } +- this.setStorageValue(key, this.storageResultTarget(result)); +- this.setStorageStatus(key, result.status, result.message || "R2 diagnostic returned without a message."); +-+ this.setStorageTiming(key, Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); +- if (result.lastChecked) { +- this.setStorageValue("lastChecked", result.lastChecked); +- this.setStorageStatus("lastChecked", "PASS", "Most recent current-environment R2 health check timestamp."); +-+ this.setStorageTiming("lastChecked", Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); +- } +- } +- +-@@ -732,15 +748,24 @@ class AdminSystemHealthController { +- runStorageDiagnostics() { +- STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { +- this.setStorageStatus(key, "PENDING", "R2 diagnostic is running through the safe Admin System Health API."); +-+ this.setStorageTiming(key, "running"); +- }); +-- STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ actionId, key }) => { +-- try { +-- this.renderStorageResult(key, runAdminSystemHealthStorageConnectivityAction(actionId)); +-- } catch (error) { +-- const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; +-+ try { +-+ const validation = runAdminSystemHealthStorageExpandedValidation(); +-+ const diagnostics = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; +-+ diagnostics.forEach((storageResult) => { +-+ const key = STORAGE_DIAGNOSTIC_ACTION_KEY_BY_ID.get(storageResult.actionId); +-+ if (key) { +-+ this.renderStorageResult(key, storageResult); +-+ } +-+ }); +-+ } catch (error) { +-+ const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; +-+ STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { +- this.setStorageStatus(key, "PENDING", message); +-- } +-- }); +-+ this.setStorageTiming(key, "not available"); +-+ }); +-+ } +- } +- +- createCell(text) { +-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md +++ open(method, url, async) { +++ this.method = method; +++ this.url = url; +++ this.async = async; +++ } +++ +++ setRequestHeader(name, value) { +++ this.headers[name] = value; +++ } +++ +++ send(body) { +++ calls.push({ +++ body, +++ headers: { ...this.headers }, +++ method: this.method, +++ url: this.url, +++ }); +++ const route = routes[`${this.method} ${this.url}`] || { +++ payload: { +++ error: `No mock route for ${this.method} ${this.url}`, +++ ok: false, +++ }, +++ status: 404, +++ }; +++ this.status = route.status; +++ this.responseText = JSON.stringify(route.payload); +++ } +++ }; +++ +++ return { +++ calls, +++ restore() { +++ if (previousWindow === undefined) { +++ delete globalThis.window; +++ } else { +++ globalThis.window = previousWindow; +++ } +++ if (previousXmlHttpRequest === undefined) { +++ delete globalThis.XMLHttpRequest; +++ } else { +++ globalThis.XMLHttpRequest = previousXmlHttpRequest; +++ } +++ clearPublicConfigCache(); +++ clearServerApiDiagnostics(); +++ }, +++ }; +++} ++ ++ test("requireServerApiData returns the server data payload", () => { ++ assert.deepEqual( ++@@ -57,3 +129,57 @@ test("requireServerApiData supports client-specific restore guidance", () => { ++ /Admin setup status did not return server data\. Restore the admin setup API\./, ++ ); ++ }); +++ +++test("logoutSessionUser uses the standardized session server data boundary", () => { +++ const mockApi = installMockServerApi({ +++ "POST http://runtime-api.test/api/session/logout": { +++ payload: { +++ data: { +++ authenticated: false, +++ userKey: "", +++ }, +++ ok: true, +++ }, +++ status: 200, +++ }, +++ }); +++ +++ try { +++ assert.deepEqual(logoutSessionUser(), { +++ authenticated: false, +++ userKey: "", +++ }); +++ assert.deepEqual(mockApi.calls, [ +++ { +++ body: null, +++ headers: { +++ Accept: "application/json", +++ }, +++ method: "POST", +++ url: "http://runtime-api.test/api/session/logout", +++ }, +++ ]); +++ } finally { +++ mockApi.restore(); +++ } +++}); +++ +++test("logoutSessionUser preserves session restore guidance for missing server data", () => { +++ const mockApi = installMockServerApi({ +++ "POST http://runtime-api.test/api/session/logout": { +++ payload: { +++ ok: true, +++ }, +++ status: 200, +++ }, +++ }); +++ +++ try { +++ assert.throws( +++ () => logoutSessionUser(), +++ /Session logout did not return server data\. Restore the server auth\/session API\./, +++ ); +++ } finally { +++ mockApi.restore(); +++ } +++}); ++diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md + new file mode 100644 +-index 000000000..61eba9efb ++index 000000000..f7d6337e0 + --- /dev/null +-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md +-@@ -0,0 +1,35 @@ +-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation +++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md ++@@ -0,0 +1,63 @@ +++# PR_26175_DELTA_007-runtime-api-client-service-tests +++ +++## Summary + + +-+Team: Charlie +-+Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation +-+Base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel +-+Lifecycle: Build / Validation +-+Repair: Rebased onto repaired PR_26177_CHARLIE_029 branch on 2026-06-25. +++Team Delta added service-level API client coverage for the standardized Browser -> Server API -> Data Source boundary. +++ +++This PR fixes the session logout API client to use the same `requireServerApiData(...)` unwrap path as the other session client calls. It also adds targeted tests proving logout uses the configured server API URL, returns `payload.data`, and preserves the session-specific restore guidance when server data is missing. + + + +## Scope +-+- Expanded System Health R2 validation into an API-owned current-environment validation run. +-+- Added safe list/upload/read/delete diagnostics with duration, operation label, cleanup status, and permanent-object metadata. +-+- Updated the System Health UI to show storage timing without adding inline styles, script blocks, or page-local CSS. +-+ +-+## Changed Files +-+- admin/system-health.html +-+- assets/theme-v2/js/admin-system-health.js +-+- src/api/admin-system-health-api-client.js +-+- src/dev-runtime/server/local-api-router.mjs +-+- tests/api/admin-system-health/contract.test.mjs +-+- tests/dev-runtime/AdminHealthOperations.test.mjs +-+- tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +-+- docs_build/dev/reports/coverage_changed_js_guardrail.tx +-+- docs_build/dev/reports/playwright_v8_coverage_report.tx +-+ +-+## Validation +-+- PASS: node --check src/dev-runtime/server/local-api-router.mjs +-+- PASS: node --check assets/theme-v2/js/admin-system-health.js +-+- PASS: node --check src/api/admin-system-health-api-client.js +-+- PASS: node --test tests/api/admin-system-health/contract.test.mjs +-+- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs +-+- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line +-+- PASS: git diff --check +++ +++- Team: Delta +++- Branch: `PR_26175_DELTA_007-runtime-api-client-service-tests` +++- Runtime file changed: `src/api/session-api-client.js` +++- Test file changed: `tests/dev-runtime/ServerApiClientStandardization.test.mjs` +++- Service test command added: `npm run test:service:api-client` +++- Site-wide command preserved: `npm test` +++ +++## Runtime Impact +++ +++PASS - Session logout now follows the standardized server-data boundary instead of calling an undefined helper. +++ +++## API / Data Impact +++ +++PASS - No API routes or persisted data contracts changed. The browser API client still calls `/session/logout` with `POST` and reads server-owned `payload.data`. +++ +++## Requirement Checklist +++ +++| Requirement | Status | Notes | +++|---|---|---| +++| One PR purpose only | PASS | API client service testability only. | +++| Team Delta ownership only | PASS | API clients and runtime test coverage are Delta-owned. | +++| No team-specific test runner | PASS | No Delta-named runner or command added. | +++| No `scripts/run-delta-runtime-validation.mjs` | PASS | File was not added. | +++| No `test:delta-runtime` | PASS | Script was not added. | +++| Testing organized by service/page level | PASS | Added `test:service:api-client`. | +++| Keep `npm test` as site-wide command | PASS | Existing `npm test` is unchanged. | +++| No UI changes | PASS | No UI files changed. | +++| No browser-owned product data | PASS | Test stubs only the server API response; no persisted browser data source added. | +++| No silent fallbacks or hidden defaults | PASS | Missing server data still throws explicit restore guidance. | +++ +++## Validation Lane Report +++ +++| Command | Status | Notes | +++|---|---|---| +++| `node --check src/api/session-api-client.js` | PASS | API client syntax valid. | +++| `node --check tests/dev-runtime/ServerApiClientStandardization.test.mjs` | PASS | Test syntax valid. | +++| `npm run test:service:api-client` | PASS | 1 targeted file, 6 tests passed. | +++| `git diff --check` | PASS | No whitespace errors. | +++| `npm run codex:review-artifacts` | PASS | Regenerated `codex_review.diff` and `codex_changed_files.txt`. | +++ +++## Manual Validation Notes +++ +++- Confirmed `package.json` keeps `npm test` unchanged. +++- Confirmed no Team Delta-specific validation command was introduced. +++- Confirmed session logout test uses mocked server API data instead of browser-owned persisted state. +++- Playwright was not run; this is a Node service/API-client coverage change. + + + +## ZIP +-+- Generated after repair: C:\Users\DavidQ\Documents\GitHub\HTML-JavaScript-Gaming\tmp\PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_delta.zip +-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md +++ +++Expected repo-structured delta ZIP: +++ +++`tmp/PR_26175_DELTA_007-runtime-api-client-service-tests_delta.zip` +++ ++diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md + new file mode 100644 +-index 000000000..0c0387294 ++index 000000000..d8dc22bf1 + --- /dev/null +-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md +-@@ -0,0 +1,17 @@ +-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Branch Validation +-+ +-+Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation +-+Expected stack base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel +-+Current status at validation: +-+## pr/26177-CHARLIE-030-r2-storage-health-expanded-validation +-+Updated onto repaired PR_26177_CHARLIE_029 stack base. +-+ +-+Result: PASS +-+ +-+Checks: +-+- PASS: Branch created from PR 029 branch for the approved stacked chain. +-+- PASS: Active branch matches PR identity. +-+- PASS: Worktree contained only scoped PR changes and generated validation reports. +-+- PASS: Branch is based on repaired PR 029 branch. +-+- PASS: Rebase conflict scope was generated report artifacts only. +-+- PASS: No start_of_day files modified. +-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md +++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md ++@@ -0,0 +1,10 @@ +++# PR_26175_DELTA_007 Branch Validation +++ +++| Check | Status | Evidence | +++|---|---|---| +++| Started from `main` | PASS | `main` was checked out and fast-forward pulled before work began. | +++| Local/origin sync before branch | PASS | `git rev-list --left-right --count main...origin/main` returned `0 0`. | +++| Worktree clean before branch | PASS | `git status --short` returned no entries. | +++| Working branch | PASS | `PR_26175_DELTA_007-runtime-api-client-service-tests`. | +++| Direct commit to `main` avoided | PASS | Changes were made only on the PR branch. | +++ ++diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md + new file mode 100644 +-index 000000000..36806ff77 ++index 000000000..13bd9f8b5 + --- /dev/null +-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md +++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md + @@ -0,0 +1,9 @@ +-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Manual Validation Notes +-+ +-+- Confirmed the browser calls one expanded R2 validation action instead of owning the list/upload/read/delete sequence itself. +-+- Confirmed Storage Health displays timing values for list, upload, read, and delete rows. +-+- Confirmed storage validation payload includes cleanup status and permanent-object metadata. +-+- Confirmed storage diagnostics stay scoped to the current environment folder. +-+- Confirmed no storage credentials or secrets are displayed. +-+- Confirmed branch repair conflict was limited to generated report artifacts. +-+- Confirmed no start_of_day files changed. +-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md +++# PR_26175_DELTA_007 Manual Validation Notes +++ +++- Reviewed changed files for API-client-only scope. +++- Confirmed no UI files changed. +++- Confirmed no persisted browser-owned product data was introduced. +++- Confirmed no `scripts/run-delta-runtime-validation.mjs` file was added. +++- Confirmed no `test:delta-runtime` script was added. +++- Confirmed `npm test` remains the site-wide/all-tests command. +++ ++diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md + new file mode 100644 +-index 000000000..2dda36cc8 ++index 000000000..5da7573e5 + --- /dev/null +-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md +-@@ -0,0 +1,13 @@ +-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Requirement Checklis +-+ +-+- PASS: Expanded Cloud/R2 storage health validation. +-+- PASS: Included safe bucket/list/write/read/delete validation through API-owned action contract. +-+- PASS: Included timing/status fields where available. +-+- PASS: Does not create browser-owned storage health state. +-+- PASS: Test object cleanup is attempted in the same expanded validation run. +-+- PASS: Does not expose secrets. +-+- PASS: Current environment folder only. +-+- PASS: No unrelated files modified. +-+- PASS: No start_of_day files modified. +-+- PASS: Rebased onto repaired PR_26177_CHARLIE_029 branch. +-+- PASS: Shows unavailable/not configured states explicitly. +-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md +++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md ++@@ -0,0 +1,12 @@ +++# PR_26175_DELTA_007 Requirements Checklist +++ +++| Requirement | Status | Notes | +++|---|---|---| +++| Branch from updated main | PASS | Branch created after fast-forward pull and sync check. | +++| Build smallest scoped change | PASS | Fixed session logout unwrap and added focused API client tests. | +++| Validate targeted tests | PASS | `npm run test:service:api-client` passed. | +++| Required reports created | PASS | PR report, branch validation, checklist, validation lane, manual notes, review diff, changed files. | +++| Repo-structured ZIP created | PENDING | Created after report files are staged in the delta package step. | +++| Open PR | PENDING | Opened after commit and push. | +++| Stop before merge approval | PASS | This PR is draft/open only; no merge performed. | +++ ++diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md + new file mode 100644 +-index 000000000..0dbf85ec0 ++index 000000000..f3844a691 + --- /dev/null +-+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md +-@@ -0,0 +1,22 @@ +-+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Validation Lane Repor +-+ +-+Impacted lanes: +-+- runtime: Local API Admin System Health R2 action contract. +-+- contract: Admin System Health API contract tests. +-+- UI: Admin System Health Theme V2 page controller and markup. +-+- Playwright: targeted Admin System Health page spec. +-+ +-+Commands: +-+- PASS: node --check src/dev-runtime/server/local-api-router.mjs +-+- PASS: node --check assets/theme-v2/js/admin-system-health.js +-+- PASS: node --check src/api/admin-system-health-api-client.js +-+- PASS: node --test tests/api/admin-system-health/contract.test.mjs +-+- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs +-+- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line +-+- PASS: git diff --check +-+ +-+Skipped lanes: +-+- Full samples smoke skipped; not impacted by System Health storage diagnostics. +-+- Full Project Workspace suite skipped; targeted Admin/System Health coverage was sufficient for this scoped page/API change. +-+ +-+Result: PASS +-diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.tx +-index 7b1c51f19..7211f2375 100644 +---- a/docs_build/dev/reports/coverage_changed_js_guardrail.tx +-+++ b/docs_build/dev/reports/coverage_changed_js_guardrail.tx +-@@ -6,7 +6,9 @@ Missing changed runtime JS files are WARN, not FAIL. +- Source: Playwright/Chromium built-in V8 coverage from the active Playwright run. +- +- Changed runtime JS files considered: +--(100%) none changed - no changed runtime JS files +-+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +-+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 +-+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 +- +- Guardrail warnings: +--(100%) none changed - no changed runtime JS files +-+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file missing from coverage; advisory only +-diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.tx +-index 627ed631f..2a2982e6a 100644 +---- a/docs_build/dev/reports/playwright_v8_coverage_report.tx +-+++ b/docs_build/dev/reports/playwright_v8_coverage_report.tx +-@@ -17,7 +17,9 @@ Exercised tool entry points detected: +- (78%) Theme V2 Shared JS - exercised 5 runtime JS files +- +- Changed runtime JS files covered: +--(100%) none changed - no changed runtime JS files +-+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +-+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 +-+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 +- +- Files with executed line/function counts where available: +- (36%) src/api/server-api-client.js - executed lines 168/168; executed functions 5/14 +-@@ -27,13 +29,18 @@ Files with executed line/function counts where available: +- (77%) assets/theme-v2/js/tool-display-mode.js - executed lines 304/304; executed functions 23/30 +- (80%) assets/theme-v2/js/theme-icons.js - executed lines 69/69; executed functions 4/5 +- (80%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 4/5 +--(81%) assets/theme-v2/js/admin-system-health.js - executed lines 848/848; executed functions 70/86 +-+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 +- (83%) assets/js/shared/status.js - executed lines 37/37; executed functions 5/6 +- (91%) assets/theme-v2/js/admin-owner-navigation.js - executed lines 58/58; executed functions 10/11 +--(100%) src/api/admin-system-health-api-client.js - executed lines 28/28; executed functions 4/4 +-+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 +- +- Uncovered or low-coverage changed JS files: +--(100%) none changed - no changed runtime JS files +-+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: uncovered changed runtime JS file; advisory only +- +- Changed JS files considered: +--(100%) none - no changed JS files +-+(0%) src/dev-runtime/server/local-api-router.mjs - changed JS file not collected as browser runtime coverage +-+(0%) tests/api/admin-system-health/contract.test.mjs - changed JS file not collected as browser runtime coverage +-+(0%) tests/dev-runtime/AdminHealthOperations.test.mjs - changed JS file not collected as browser runtime coverage +-+(0%) tests/playwright/tools/AdminHealthOperationsPage.spec.mjs - changed JS file not collected as browser runtime coverage +-+(81%) assets/theme-v2/js/admin-system-health.js - changed JS file with browser V8 coverage +-+(100%) src/api/admin-system-health-api-client.js - changed JS file with browser V8 coverage +-diff --git a/src/api/admin-system-health-api-client.js b/src/api/admin-system-health-api-client.js +-index 9b166d097..49101c59f 100644 +---- a/src/api/admin-system-health-api-client.js +-+++ b/src/api/admin-system-health-api-client.js +-@@ -20,6 +20,10 @@ export function runAdminSystemHealthStorageConnectivityAction(actionId) { +- ); +- } +- +-+export function runAdminSystemHealthStorageExpandedValidation() { +-+ return runAdminSystemHealthStorageConnectivityAction("storage-expanded-validation"); +-+} +++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md ++@@ -0,0 +1,20 @@ +++# PR_26175_DELTA_007 Validation Lane + + +- export function runAdminSystemHealthAction(actionId) { +- return requireServerApiData( +- safeRequestServerApi("/admin/system-health/action", { +-diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs +-index 462d8b7a0..6dbb8a106 100644 +---- a/src/dev-runtime/server/local-api-router.mjs +-+++ b/src/dev-runtime/server/local-api-router.mjs +-@@ -356,6 +356,7 @@ const SYSTEM_HEALTH_STORAGE_ACTION_IDS = Object.freeze([ +- "storage-read-test-object", +- "storage-delete-test-object", +- ]); +-+const SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID = "storage-expanded-validation"; +- const SYSTEM_HEALTH_MANUAL_ACTION_LABELS = Object.freeze({ +- "database-check": "Run Database Check", +- "full-health-check": "Run Full Health Check", +-@@ -4046,43 +4047,75 @@ class ApiRuntimeDataSource { +- +- storageConnectivityConfigFailure(actionId, storage, scope = "project-prefix") { +- const missing = storage.config?.missingKeys?.join(", ") || storage.config?.validationError || "storage configuration incomplete"; +-+ const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); +- return { +- actionId, +-+ cleanupStatus: "not-run", +-+ durationMs: 0, +- environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", +- executed: false, +- lastChecked: new Date().toISOString(), +- message: `Storage connectivity requires configured storage and GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX before this action can run. Missing or invalid: ${missing}.`, +-+ operation: action?.operation || "configuration", +-+ operationLabel: action?.label || "Storage configuration", +-+ permanentObjectCreated: false, +- projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), +- secretEditingAllowed: false, +- secretsExposed: false, +- status: "FAIL", +- storageStatus: this.ownerStorageStatus(), +-+ targetPrefix: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : String(storage.config?.projectsPrefix || "").trim(), +- }; +- } +- +-- storageConnectivityResult({ actionId, environmentFolder = "", executed = true, keysListed = 0, message, objectKey, projectsPrefix, status }) { +-+ storageConnectivityResult({ +-+ action, +-+ actionId, +-+ cleanupStatus = "not-applicable", +-+ durationMs = 0, +-+ environmentFolder = "", +-+ executed = true, +-+ keysListed = 0, +-+ message, +-+ objectKey, +-+ permanentObjectCreated = false, +-+ projectsPrefix, +-+ status, +-+ targetPrefix = "", +-+ }) { +-+ const resolvedAction = action || STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId) || {}; +- return { +- actionId, +-+ cleanupStatus, +-+ durationMs, +- environmentFolder, +- executed, +- keysListed, +- lastChecked: new Date().toISOString(), +- message, +-+ operation: resolvedAction.operation || "", +-+ operationLabel: resolvedAction.label || actionId, +-+ permanentObjectCreated, +- projectsPrefix, +- secretEditingAllowed: false, +- secretsExposed: false, +- status, +- storageStatus: this.ownerStorageStatus(), +-+ targetPrefix, +- testObjectKey: objectKey, +- }; +- } +- +- async runStorageConnectivityAction(actionId, options = {}) { +-+ if (actionId === SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID) { +-+ return this.runStorageExpandedValidation(options); +-+ } +- const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); +- if (!action) { +- throw new Error(`Unknown storage connectivity action: ${actionId || "missing actionId"}.`); +- } +- +-+ const startedAt = Date.now(); +- const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; +- const storage = createConfiguredProjectAssetStorage(); +- const projectsPrefix = String(storage.config?.projectsPrefix || "").trim(); +-@@ -4098,7 +4131,9 @@ class ApiRuntimeDataSource { +- const result = await storage.listObjects(targetPrefix); +- const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; +- return this.storageConnectivityResult({ +-+ action, +- actionId: action.id, +-+ durationMs: Date.now() - startedAt, +- environmentFolder, +- keysListed, +- message: result.ok +-@@ -4107,6 +4142,7 @@ class ApiRuntimeDataSource { +- objectKey, +- projectsPrefix, +- status: result.ok ? "PASS" : "FAIL", +-+ targetPrefix, +- }); +- } +- +-@@ -4116,7 +4152,9 @@ class ApiRuntimeDataSource { +- : await storage.listProjectObjects(); +- const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; +- return this.storageConnectivityResult({ +-+ action, +- actionId: action.id, +-+ durationMs: Date.now() - startedAt, +- environmentFolder, +- keysListed, +- message: result.ok +-@@ -4125,6 +4163,7 @@ class ApiRuntimeDataSource { +- objectKey, +- projectsPrefix, +- status: result.ok ? "PASS" : "FAIL", +-+ targetPrefix, +- }); +- } +- +-@@ -4135,14 +4174,19 @@ class ApiRuntimeDataSource { +- objectKey, +- }); +- return this.storageConnectivityResult({ +-+ action, +- actionId: action.id, +-+ cleanupStatus: result.ok ? "requires-delete-action" : "not-created", +-+ durationMs: Date.now() - startedAt, +- environmentFolder, +- message: result.ok +- ? `Upload test object completed at ${objectKey}. +- : `${result.message || "Storage upload failed."} Verify upload permission for ${targetPrefix}.`, +- objectKey, +-+ permanentObjectCreated: result.ok === true, +- projectsPrefix, +- status: result.ok ? "PASS" : "FAIL", +-+ targetPrefix, +- }); +- } +- +-@@ -4151,7 +4195,9 @@ class ApiRuntimeDataSource { +- const text = result.ok ? Buffer.from(result.bytes || []).toString("utf8") : ""; +- const contentMatches = text === STORAGE_CONNECTIVITY_TEST_OBJECT_CONTENT; +- return this.storageConnectivityResult({ +-+ action, +- actionId: action.id, +-+ durationMs: Date.now() - startedAt, +- environmentFolder, +- message: result.ok && contentMatches +- ? `Read test object completed from ${objectKey}. +-@@ -4159,32 +4205,77 @@ class ApiRuntimeDataSource { +- objectKey, +- projectsPrefix, +- status: result.ok && contentMatches ? "PASS" : "FAIL", +-+ targetPrefix, +- }); +- } +- +- const result = await storage.deleteObject(objectKey); +- return this.storageConnectivityResult({ +-+ action, +- actionId: action.id, +-+ cleanupStatus: result.ok ? "completed" : "failed", +-+ durationMs: Date.now() - startedAt, +- environmentFolder, +- message: result.ok +- ? `Delete test object completed at ${objectKey}. +- : `${result.message || "Storage delete failed."} Verify delete permission for ${targetPrefix}.`, +- objectKey, +-+ permanentObjectCreated: false, +- projectsPrefix, +- status: result.ok ? "PASS" : "FAIL", +-+ targetPrefix, +- }); +- } catch (error) { +- return this.storageConnectivityResult({ +-+ action, +- actionId: action.id, +-+ durationMs: Date.now() - startedAt, +- environmentFolder, +- message: `Storage connectivity action failed: ${error instanceof Error ? error.message : String(error || "Unknown storage error.")}`, +- objectKey, +- projectsPrefix, +- status: "FAIL", +-+ targetPrefix, +- }); +- } +- } +- +-+ async runStorageExpandedValidation(options = {}) { +-+ const startedAt = Date.now(); +-+ const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; +-+ const storage = createConfiguredProjectAssetStorage(); +-+ const targetPrefix = this.storageConnectivityTargetPrefix(storage, scope); +-+ const objectKey = this.storageConnectivityTestObjectKey(storage, scope); +-+ const results = []; +-+ for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { +-+ results.push(await this.runStorageConnectivityAction(actionId, { scope })); +-+ } +-+ const uploadResult = results.find((result) => result.actionId === "storage-upload-test-object"); +-+ const deleteResult = results.find((result) => result.actionId === "storage-delete-test-object"); +-+ const permanentObjectCreated = uploadResult?.status === "PASS" && deleteResult?.status !== "PASS"; +-+ const status = overallHealthStatus(results); +-+ return { +-+ actionId: SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID, +-+ cleanupStatus: permanentObjectCreated ? "failed" : "completed", +-+ durationMs: Date.now() - startedAt, +-+ environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", +-+ executed: true, +-+ lastChecked: new Date().toISOString(), +-+ message: permanentObjectCreated +-+ ? `Expanded R2 validation detected a cleanup failure for ${objectKey}; review storage credentials and delete permission. +-+ : `Expanded R2 validation completed against ${targetPrefix}; test object cleanup was attempted in the same validation run.`, +-+ permanentObjectCreated, +-+ projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), +-+ secretEditingAllowed: false, +-+ secretsExposed: false, +-+ status, +-+ storageDiagnostics: results, +-+ storageStatus: this.ownerStorageStatus(), +-+ targetPrefix, +-+ testObjectKey: objectKey, +-+ }; +-+ } +++## Targeted Lane +++ +++`npm run test:service:api-client` +++ +++## Results +++ +++| Command | Result | +++|---|---| +++| `node --check src/api/session-api-client.js` | PASS | +++| `node --check tests/dev-runtime/ServerApiClientStandardization.test.mjs` | PASS | +++| `npm run test:service:api-client` | PASS | +++| `git diff --check` | PASS | +++ +++## Notes +++ +++- The service lane is page/service-level and not named for Team Delta. +++- Full `npm test` was not run because this PR is scoped to API client service coverage. + + +- ownerPromotionFoundation() { +- const steps = [ +- { +-@@ -4424,20 +4515,20 @@ SELECT pg_database_size(current_database()) AS database_size_bytes, +- } +- +- async adminSystemHealthStorageHealthCheck() { +-- const results = []; +-- for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { +-- results.push(await this.runStorageConnectivityAction(actionId, { scope: "environment-folder" })); +-- } +-+ const validation = await this.runStorageExpandedValidation({ scope: "environment-folder" }); +-+ const results = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; +- return { +- actionId: "storage-check", +- checkedAt: new Date().toISOString(), +- label: SYSTEM_HEALTH_MANUAL_ACTION_LABELS["storage-check"], +-- message: "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", +-+ message: validation.message || "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", +-+ permanentObjectCreated: validation.permanentObjectCreated === true, +- secretEditingAllowed: false, +- secretsExposed: false, +-- status: overallHealthStatus(results.map((result) => ({ status: result.status }))), +-+ status: validation.status || overallHealthStatus(results.map((result) => ({ status: result.status }))), +- storageDiagnostics: results, +- storageStatus: this.ownerStorageStatus(), +-+ validationDurationMs: validation.durationMs, +- }; +- } +- +-diff --git a/tests/api/admin-system-health/contract.test.mjs b/tests/api/admin-system-health/contract.test.mjs +-index 25aa84b34..54e821f13 100644 +---- a/tests/api/admin-system-health/contract.test.mjs +-+++ b/tests/api/admin-system-health/contract.test.mjs +-@@ -146,6 +146,25 @@ test("Admin System Health completion contract remains server-owned and current-e +- assert.equal(healthText.includes("/uat/projects"), false); +- assert.equal(health.secretEditingAllowed, false); +- assert.equal(health.secretsExposed, false); +-+ const expandedStorage = await apiJson(server.baseUrl, "/api/admin/system-health/storage-connectivity-action", { +-+ body: { actionId: "storage-expanded-validation" }, +-+ method: "POST", +-+ }); +-+ assert.deepEqual( +-+ expandedStorage.storageDiagnostics.map((row) => row.actionId), +-+ [ +-+ "storage-bucket-connectivity", +-+ "storage-list", +-+ "storage-upload-test-object", +-+ "storage-read-test-object", +-+ "storage-delete-test-object", +-+ ], +-+ ); +-+ assert.equal(expandedStorage.storageDiagnostics.every((row) => row.environmentFolder === "/dev"), true); +-+ assert.equal(expandedStorage.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); +-+ assert.equal(expandedStorage.permanentObjectCreated, false); +-+ assert.equal(JSON.stringify(expandedStorage).includes("api-secret"), false); +-+ assert.equal(JSON.stringify(expandedStorage).includes("site-secret"), false); +- } finally { +- await server.close(); +- } +-diff --git a/tests/dev-runtime/AdminHealthOperations.test.mjs b/tests/dev-runtime/AdminHealthOperations.test.mjs +-index 99fef0757..9ea40988a 100644 +---- a/tests/dev-runtime/AdminHealthOperations.test.mjs +-+++ b/tests/dev-runtime/AdminHealthOperations.test.mjs +-@@ -311,6 +311,12 @@ test("Admin can view operational health while Creator sessions are blocked", asy +- ], +- ); +- assert.equal(storageAction.storageDiagnostics.every((row) => row.environmentFolder === "/local"), true); +-+ assert.equal(storageAction.permanentObjectCreated, false); +-+ assert.equal(typeof storageAction.validationDurationMs, "number"); +-+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); +-+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.operationLabel === "string"), true); +-+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.cleanupStatus === "string"), true); +-+ assert.equal(storageAction.storageDiagnostics.every((row) => row.permanentObjectCreated === false), true); +- const refreshAction = await apiJson(server.baseUrl, "/api/admin/system-health/action", { +- body: { actionId: "refresh" }, +- method: "POST", +-diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +-index 56733ce6a..713344ac0 100644 +---- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +-+++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs +-@@ -233,6 +233,10 @@ test("Admin System Health renders Postgres diagnostics through the safe status A +- await expect(page.locator("[data-admin-system-health-storage-value='upload']")).toContainText("/dev"); +- await expect(page.locator("[data-admin-system-health-storage-value='read']")).not.toHaveText("Health object"); +- await expect(page.locator("[data-admin-system-health-storage-value='delete']")).not.toHaveText("Health object"); +-+ await expect(page.locator("[data-admin-system-health-storage-timing='list']")).not.toHaveText("Loading"); +-+ await expect(page.locator("[data-admin-system-health-storage-timing='upload']")).not.toHaveText("Loading"); +-+ await expect(page.locator("[data-admin-system-health-storage-timing='read']")).not.toHaveText("Loading"); +-+ await expect(page.locator("[data-admin-system-health-storage-timing='delete']")).not.toHaveText("Loading"); +- await expect(page.getByRole("table", { name: "Runtime health" })).toContainText("Runtime Health"); +- await expect(page.locator("[data-admin-system-health-runtime-health-value='environment']")).toHaveText("DEV"); +- await expect(page.locator("[data-admin-system-health-runtime-health-value='appVersion']")).toHaveText("1.0.0"); +-@@ -286,7 +290,7 @@ test("Admin System Health renders Postgres diagnostics through the safe status A +- expect((title || ariaLabel || "").trim()).not.toEqual(""); +- } +- expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/status"))).toBe(true); +-- expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(5); +-+ expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(1); +- await page.getByRole("button", { name: "Run Runtime Check" }).click(); +- await expect(page.getByRole("table", { name: "Manual health action results" })).toContainText("Run Runtime Check"); +- expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/action"))).toBe(true); +-@@ -353,5 +357,5 @@ test("Admin System Health operations page keeps scripts and styles external", as +- expect(runtimeSource).not.toContain("localStorage"); +- expect(runtimeSource).not.toContain("sessionStorage"); +- expect(runtimeSource).toContain("runAdminSystemHealthAction"); +-- expect(runtimeSource).toContain("runAdminSystemHealthStorageConnectivityAction"); +-+ expect(runtimeSource).toContain("runAdminSystemHealthStorageExpandedValidation"); +- }); +diff --git a/package.json b/package.json +index 72c3c9c32..05118574a 100644 +--- a/package.json ++++ b/package.json +@@ -3,6 +3,7 @@ + "scripts": { + "pretest": "node docs_build/dev/toolbox/checkSharedExtractionGuard.mjs", + "test": "node ./scripts/run-node-tests.mjs", ++ "test:service:api-client": "node ./scripts/run-node-test-files.mjs tests/dev-runtime/ServerApiClientStandardization.test.mjs", + "test:audit:locations": "node ./scripts/audit-playwright-test-locations.mjs", + "test:playwright:structure": "node ./scripts/audit-playwright-test-locations.mjs", + "test:playwright:zero-browser": "node ./scripts/run-targeted-test-lanes.mjs --zero-browser-only", +diff --git a/src/api/session-api-client.js b/src/api/session-api-client.js +index f31004ea1..3b8c69125 100644 +--- a/src/api/session-api-client.js ++++ b/src/api/session-api-client.js +@@ -51,7 +51,7 @@ export function signIn(options = {}) { + } + + export function logoutSessionUser() { +- return unwrap(safeRequestServerApi("/session/logout", { method: "POST" }), "Session logout"); ++ return requireSessionApiData(safeRequestServerApi("/session/logout", { method: "POST" }), "Session logout"); + } - renderRuntimeHealthPending(reason) { -@@ -684,9 +698,11 @@ class AdminSystemHealthController { - } - this.setStorageValue(key, this.storageResultTarget(result)); - this.setStorageStatus(key, result.status, result.message || "R2 diagnostic returned without a message."); -+ this.setStorageTiming(key, Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); - if (result.lastChecked) { - this.setStorageValue("lastChecked", result.lastChecked); - this.setStorageStatus("lastChecked", "PASS", "Most recent current-environment R2 health check timestamp."); -+ this.setStorageTiming("lastChecked", Number.isFinite(Number(result.durationMs)) ? `${result.durationMs} ms` : "not available"); - } - } + export function signOut() { +diff --git a/tests/dev-runtime/ServerApiClientStandardization.test.mjs b/tests/dev-runtime/ServerApiClientStandardization.test.mjs +index 5530e4074..a694fa78e 100644 +--- a/tests/dev-runtime/ServerApiClientStandardization.test.mjs ++++ b/tests/dev-runtime/ServerApiClientStandardization.test.mjs +@@ -1,10 +1,82 @@ + import assert from "node:assert/strict"; + import test from "node:test"; -@@ -732,15 +748,24 @@ class AdminSystemHealthController { - runStorageDiagnostics() { - STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { - this.setStorageStatus(key, "PENDING", "R2 diagnostic is running through the safe Admin System Health API."); -+ this.setStorageTiming(key, "running"); - }); -- STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ actionId, key }) => { -- try { -- this.renderStorageResult(key, runAdminSystemHealthStorageConnectivityAction(actionId)); -- } catch (error) { -- const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; -+ try { -+ const validation = runAdminSystemHealthStorageExpandedValidation(); -+ const diagnostics = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; -+ diagnostics.forEach((storageResult) => { -+ const key = STORAGE_DIAGNOSTIC_ACTION_KEY_BY_ID.get(storageResult.actionId); -+ if (key) { -+ this.renderStorageResult(key, storageResult); -+ } -+ }); -+ } catch (error) { -+ const message = error instanceof Error ? error.message : "Safe R2 diagnostic API is unavailable."; -+ STORAGE_DIAGNOSTIC_ACTIONS.forEach(({ key }) => { - this.setStorageStatus(key, "PENDING", message); -- } -- }); -+ this.setStorageTiming(key, "not available"); -+ }); -+ } - } ++import { clearPublicConfigCache } from "../../src/api/public-config-client.js"; + import { ++ clearServerApiDiagnostics, + requireServerApiData, + SERVER_DATA_BOUNDARY_RULE, + } from "../../src/api/server-api-client.js"; ++import { logoutSessionUser } from "../../src/api/session-api-client.js"; ++ ++function installMockServerApi(routes) { ++ const calls = []; ++ const previousWindow = globalThis.window; ++ const previousXmlHttpRequest = globalThis.XMLHttpRequest; ++ ++ clearPublicConfigCache(); ++ clearServerApiDiagnostics(); ++ ++ globalThis.window = { ++ GameFoundryPublicConfig: { ++ apiUrl: "http://runtime-api.test/api", ++ }, ++ }; ++ ++ globalThis.XMLHttpRequest = class MockXMLHttpRequest { ++ constructor() { ++ this.headers = {}; ++ this.responseText = ""; ++ this.status = 0; ++ } ++ ++ open(method, url, async) { ++ this.method = method; ++ this.url = url; ++ this.async = async; ++ } ++ ++ setRequestHeader(name, value) { ++ this.headers[name] = value; ++ } ++ ++ send(body) { ++ calls.push({ ++ body, ++ headers: { ...this.headers }, ++ method: this.method, ++ url: this.url, ++ }); ++ const route = routes[`${this.method} ${this.url}`] || { ++ payload: { ++ error: `No mock route for ${this.method} ${this.url}`, ++ ok: false, ++ }, ++ status: 404, ++ }; ++ this.status = route.status; ++ this.responseText = JSON.stringify(route.payload); ++ } ++ }; ++ ++ return { ++ calls, ++ restore() { ++ if (previousWindow === undefined) { ++ delete globalThis.window; ++ } else { ++ globalThis.window = previousWindow; ++ } ++ if (previousXmlHttpRequest === undefined) { ++ delete globalThis.XMLHttpRequest; ++ } else { ++ globalThis.XMLHttpRequest = previousXmlHttpRequest; ++ } ++ clearPublicConfigCache(); ++ clearServerApiDiagnostics(); ++ }, ++ }; ++} - createCell(text) { -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md + test("requireServerApiData returns the server data payload", () => { + assert.deepEqual( +@@ -57,3 +129,57 @@ test("requireServerApiData supports client-specific restore guidance", () => { + /Admin setup status did not return server data\. Restore the admin setup API\./, + ); + }); ++ ++test("logoutSessionUser uses the standardized session server data boundary", () => { ++ const mockApi = installMockServerApi({ ++ "POST http://runtime-api.test/api/session/logout": { ++ payload: { ++ data: { ++ authenticated: false, ++ userKey: "", ++ }, ++ ok: true, ++ }, ++ status: 200, ++ }, ++ }); ++ ++ try { ++ assert.deepEqual(logoutSessionUser(), { ++ authenticated: false, ++ userKey: "", ++ }); ++ assert.deepEqual(mockApi.calls, [ ++ { ++ body: null, ++ headers: { ++ Accept: "application/json", ++ }, ++ method: "POST", ++ url: "http://runtime-api.test/api/session/logout", ++ }, ++ ]); ++ } finally { ++ mockApi.restore(); ++ } ++}); ++ ++test("logoutSessionUser preserves session restore guidance for missing server data", () => { ++ const mockApi = installMockServerApi({ ++ "POST http://runtime-api.test/api/session/logout": { ++ payload: { ++ ok: true, ++ }, ++ status: 200, ++ }, ++ }); ++ ++ try { ++ assert.throws( ++ () => logoutSessionUser(), ++ /Session logout did not return server data\. Restore the server auth\/session API\./, ++ ); ++ } finally { ++ mockApi.restore(); ++ } ++}); +diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md new file mode 100644 -index 000000000..61eba9efb +index 000000000..f7d6337e0 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation.md -@@ -0,0 +1,35 @@ -+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation ++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests.md +@@ -0,0 +1,63 @@ ++# PR_26175_DELTA_007-runtime-api-client-service-tests ++ ++## Summary + -+Team: Charlie -+Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation -+Base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -+Lifecycle: Build / Validation -+Repair: Rebased onto repaired PR_26177_CHARLIE_029 branch on 2026-06-25. ++Team Delta added service-level API client coverage for the standardized Browser -> Server API -> Data Source boundary. ++ ++This PR fixes the session logout API client to use the same `requireServerApiData(...)` unwrap path as the other session client calls. It also adds targeted tests proving logout uses the configured server API URL, returns `payload.data`, and preserves the session-specific restore guidance when server data is missing. + +## Scope -+- Expanded System Health R2 validation into an API-owned current-environment validation run. -+- Added safe list/upload/read/delete diagnostics with duration, operation label, cleanup status, and permanent-object metadata. -+- Updated the System Health UI to show storage timing without adding inline styles, script blocks, or page-local CSS. -+ -+## Changed Files -+- admin/system-health.html -+- assets/theme-v2/js/admin-system-health.js -+- src/api/admin-system-health-api-client.js -+- src/dev-runtime/server/local-api-router.mjs -+- tests/api/admin-system-health/contract.test.mjs -+- tests/dev-runtime/AdminHealthOperations.test.mjs -+- tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -+- docs_build/dev/reports/coverage_changed_js_guardrail.tx -+- docs_build/dev/reports/playwright_v8_coverage_report.tx -+ -+## Validation -+- PASS: node --check src/dev-runtime/server/local-api-router.mjs -+- PASS: node --check assets/theme-v2/js/admin-system-health.js -+- PASS: node --check src/api/admin-system-health-api-client.js -+- PASS: node --test tests/api/admin-system-health/contract.test.mjs -+- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs -+- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line -+- PASS: git diff --check ++ ++- Team: Delta ++- Branch: `PR_26175_DELTA_007-runtime-api-client-service-tests` ++- Runtime file changed: `src/api/session-api-client.js` ++- Test file changed: `tests/dev-runtime/ServerApiClientStandardization.test.mjs` ++- Service test command added: `npm run test:service:api-client` ++- Site-wide command preserved: `npm test` ++ ++## Runtime Impact ++ ++PASS - Session logout now follows the standardized server-data boundary instead of calling an undefined helper. ++ ++## API / Data Impact ++ ++PASS - No API routes or persisted data contracts changed. The browser API client still calls `/session/logout` with `POST` and reads server-owned `payload.data`. ++ ++## Requirement Checklist ++ ++| Requirement | Status | Notes | ++|---|---|---| ++| One PR purpose only | PASS | API client service testability only. | ++| Team Delta ownership only | PASS | API clients and runtime test coverage are Delta-owned. | ++| No team-specific test runner | PASS | No Delta-named runner or command added. | ++| No `scripts/run-delta-runtime-validation.mjs` | PASS | File was not added. | ++| No `test:delta-runtime` | PASS | Script was not added. | ++| Testing organized by service/page level | PASS | Added `test:service:api-client`. | ++| Keep `npm test` as site-wide command | PASS | Existing `npm test` is unchanged. | ++| No UI changes | PASS | No UI files changed. | ++| No browser-owned product data | PASS | Test stubs only the server API response; no persisted browser data source added. | ++| No silent fallbacks or hidden defaults | PASS | Missing server data still throws explicit restore guidance. | ++ ++## Validation Lane Report ++ ++| Command | Status | Notes | ++|---|---|---| ++| `node --check src/api/session-api-client.js` | PASS | API client syntax valid. | ++| `node --check tests/dev-runtime/ServerApiClientStandardization.test.mjs` | PASS | Test syntax valid. | ++| `npm run test:service:api-client` | PASS | 1 targeted file, 6 tests passed. | ++| `git diff --check` | PASS | No whitespace errors. | ++| `npm run codex:review-artifacts` | PASS | Regenerated `codex_review.diff` and `codex_changed_files.txt`. | ++ ++## Manual Validation Notes ++ ++- Confirmed `package.json` keeps `npm test` unchanged. ++- Confirmed no Team Delta-specific validation command was introduced. ++- Confirmed session logout test uses mocked server API data instead of browser-owned persisted state. ++- Playwright was not run; this is a Node service/API-client coverage change. + +## ZIP -+- Generated after repair: C:\Users\DavidQ\Documents\GitHub\HTML-JavaScript-Gaming\tmp\PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_delta.zip -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md ++ ++Expected repo-structured delta ZIP: ++ ++`tmp/PR_26175_DELTA_007-runtime-api-client-service-tests_delta.zip` ++ +diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md new file mode 100644 -index 000000000..0c0387294 +index 000000000..d8dc22bf1 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_branch-validation.md -@@ -0,0 +1,17 @@ -+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Branch Validation -+ -+Branch: pr/26177-CHARLIE-030-r2-storage-health-expanded-validation -+Expected stack base: pr/26177-CHARLIE-029-system-health-postgres-metrics-panel -+Current status at validation: -+## pr/26177-CHARLIE-030-r2-storage-health-expanded-validation -+Updated onto repaired PR_26177_CHARLIE_029 stack base. -+ -+Result: PASS -+ -+Checks: -+- PASS: Branch created from PR 029 branch for the approved stacked chain. -+- PASS: Active branch matches PR identity. -+- PASS: Worktree contained only scoped PR changes and generated validation reports. -+- PASS: Branch is based on repaired PR 029 branch. -+- PASS: Rebase conflict scope was generated report artifacts only. -+- PASS: No start_of_day files modified. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md ++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_branch-validation.md +@@ -0,0 +1,10 @@ ++# PR_26175_DELTA_007 Branch Validation ++ ++| Check | Status | Evidence | ++|---|---|---| ++| Started from `main` | PASS | `main` was checked out and fast-forward pulled before work began. | ++| Local/origin sync before branch | PASS | `git rev-list --left-right --count main...origin/main` returned `0 0`. | ++| Worktree clean before branch | PASS | `git status --short` returned no entries. | ++| Working branch | PASS | `PR_26175_DELTA_007-runtime-api-client-service-tests`. | ++| Direct commit to `main` avoided | PASS | Changes were made only on the PR branch. | ++ +diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md new file mode 100644 -index 000000000..36806ff77 +index 000000000..13bd9f8b5 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_manual-validation-notes.md ++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_manual-validation-notes.md @@ -0,0 +1,9 @@ -+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Manual Validation Notes -+ -+- Confirmed the browser calls one expanded R2 validation action instead of owning the list/upload/read/delete sequence itself. -+- Confirmed Storage Health displays timing values for list, upload, read, and delete rows. -+- Confirmed storage validation payload includes cleanup status and permanent-object metadata. -+- Confirmed storage diagnostics stay scoped to the current environment folder. -+- Confirmed no storage credentials or secrets are displayed. -+- Confirmed branch repair conflict was limited to generated report artifacts. -+- Confirmed no start_of_day files changed. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md ++# PR_26175_DELTA_007 Manual Validation Notes ++ ++- Reviewed changed files for API-client-only scope. ++- Confirmed no UI files changed. ++- Confirmed no persisted browser-owned product data was introduced. ++- Confirmed no `scripts/run-delta-runtime-validation.mjs` file was added. ++- Confirmed no `test:delta-runtime` script was added. ++- Confirmed `npm test` remains the site-wide/all-tests command. ++ +diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md new file mode 100644 -index 000000000..2dda36cc8 +index 000000000..07cd3a160 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_requirements-checklist.md -@@ -0,0 +1,13 @@ -+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Requirement Checklis -+ -+- PASS: Expanded Cloud/R2 storage health validation. -+- PASS: Included safe bucket/list/write/read/delete validation through API-owned action contract. -+- PASS: Included timing/status fields where available. -+- PASS: Does not create browser-owned storage health state. -+- PASS: Test object cleanup is attempted in the same expanded validation run. -+- PASS: Does not expose secrets. -+- PASS: Current environment folder only. -+- PASS: No unrelated files modified. -+- PASS: No start_of_day files modified. -+- PASS: Rebased onto repaired PR_26177_CHARLIE_029 branch. -+- PASS: Shows unavailable/not configured states explicitly. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md ++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_requirements-checklist.md +@@ -0,0 +1,11 @@ ++# PR_26175_DELTA_007 Requirements Checklist ++ ++| Requirement | Status | Notes | ++|---|---|---| ++| Branch from updated main | PASS | Branch created after fast-forward pull and sync check. | ++| Build smallest scoped change | PASS | Fixed session logout unwrap and added focused API client tests. | ++| Validate targeted tests | PASS | `npm run test:service:api-client` passed. | ++| Required reports created | PASS | PR report, branch validation, checklist, validation lane, manual notes, review diff, changed files. | ++| Repo-structured ZIP created | PASS | `tmp/PR_26175_DELTA_007-runtime-api-client-service-tests_delta.zip`. | ++| Open PR | PENDING | Opened after commit and push. | ++| Stop before merge approval | PASS | This PR is draft/open only; no merge performed. | +diff --git a/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md new file mode 100644 -index 000000000..0dbf85ec0 +index 000000000..f3844a691 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_030-r2-storage-health-expanded-validation_validation-lane.md -@@ -0,0 +1,22 @@ -+# PR_26177_CHARLIE_030-r2-storage-health-expanded-validation Validation Lane Repor -+ -+Impacted lanes: -+- runtime: Local API Admin System Health R2 action contract. -+- contract: Admin System Health API contract tests. -+- UI: Admin System Health Theme V2 page controller and markup. -+- Playwright: targeted Admin System Health page spec. -+ -+Commands: -+- PASS: node --check src/dev-runtime/server/local-api-router.mjs -+- PASS: node --check assets/theme-v2/js/admin-system-health.js -+- PASS: node --check src/api/admin-system-health-api-client.js -+- PASS: node --test tests/api/admin-system-health/contract.test.mjs -+- PASS: node --test tests/dev-runtime/AdminHealthOperations.test.mjs -+- PASS: npx playwright test tests/playwright/tools/AdminHealthOperationsPage.spec.mjs --workers=1 --reporter=line -+- PASS: git diff --check -+ -+Skipped lanes: -+- Full samples smoke skipped; not impacted by System Health storage diagnostics. -+- Full Project Workspace suite skipped; targeted Admin/System Health coverage was sufficient for this scoped page/API change. -+ -+Result: PASS -diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.tx -index 7b1c51f19..7211f2375 100644 ---- a/docs_build/dev/reports/coverage_changed_js_guardrail.tx -+++ b/docs_build/dev/reports/coverage_changed_js_guardrail.tx -@@ -6,7 +6,9 @@ Missing changed runtime JS files are WARN, not FAIL. - Source: Playwright/Chromium built-in V8 coverage from the active Playwright run. - - Changed runtime JS files considered: --(100%) none changed - no changed runtime JS files -+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 -+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 - - Guardrail warnings: --(100%) none changed - no changed runtime JS files -+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file missing from coverage; advisory only -diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.tx -index 627ed631f..2a2982e6a 100644 ---- a/docs_build/dev/reports/playwright_v8_coverage_report.tx -+++ b/docs_build/dev/reports/playwright_v8_coverage_report.tx -@@ -17,7 +17,9 @@ Exercised tool entry points detected: - (78%) Theme V2 Shared JS - exercised 5 runtime JS files - - Changed runtime JS files covered: --(100%) none changed - no changed runtime JS files -+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only -+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 -+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 - - Files with executed line/function counts where available: - (36%) src/api/server-api-client.js - executed lines 168/168; executed functions 5/14 -@@ -27,13 +29,18 @@ Files with executed line/function counts where available: - (77%) assets/theme-v2/js/tool-display-mode.js - executed lines 304/304; executed functions 23/30 - (80%) assets/theme-v2/js/theme-icons.js - executed lines 69/69; executed functions 4/5 - (80%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 4/5 --(81%) assets/theme-v2/js/admin-system-health.js - executed lines 848/848; executed functions 70/86 -+(81%) assets/theme-v2/js/admin-system-health.js - executed lines 872/872; executed functions 72/89 - (83%) assets/js/shared/status.js - executed lines 37/37; executed functions 5/6 - (91%) assets/theme-v2/js/admin-owner-navigation.js - executed lines 58/58; executed functions 10/11 --(100%) src/api/admin-system-health-api-client.js - executed lines 28/28; executed functions 4/4 -+(100%) src/api/admin-system-health-api-client.js - executed lines 31/31; executed functions 5/5 - - Uncovered or low-coverage changed JS files: --(100%) none changed - no changed runtime JS files -+(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: uncovered changed runtime JS file; advisory only - - Changed JS files considered: --(100%) none - no changed JS files -+(0%) src/dev-runtime/server/local-api-router.mjs - changed JS file not collected as browser runtime coverage -+(0%) tests/api/admin-system-health/contract.test.mjs - changed JS file not collected as browser runtime coverage -+(0%) tests/dev-runtime/AdminHealthOperations.test.mjs - changed JS file not collected as browser runtime coverage -+(0%) tests/playwright/tools/AdminHealthOperationsPage.spec.mjs - changed JS file not collected as browser runtime coverage -+(81%) assets/theme-v2/js/admin-system-health.js - changed JS file with browser V8 coverage -+(100%) src/api/admin-system-health-api-client.js - changed JS file with browser V8 coverage -diff --git a/src/api/admin-system-health-api-client.js b/src/api/admin-system-health-api-client.js -index 9b166d097..49101c59f 100644 ---- a/src/api/admin-system-health-api-client.js -+++ b/src/api/admin-system-health-api-client.js -@@ -20,6 +20,10 @@ export function runAdminSystemHealthStorageConnectivityAction(actionId) { - ); - } - -+export function runAdminSystemHealthStorageExpandedValidation() { -+ return runAdminSystemHealthStorageConnectivityAction("storage-expanded-validation"); -+} ++++ b/docs_build/dev/reports/PR_26175_DELTA_007-runtime-api-client-service-tests_validation-lane.md +@@ -0,0 +1,20 @@ ++# PR_26175_DELTA_007 Validation Lane + - export function runAdminSystemHealthAction(actionId) { - return requireServerApiData( - safeRequestServerApi("/admin/system-health/action", { -diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs -index 462d8b7a0..6dbb8a106 100644 ---- a/src/dev-runtime/server/local-api-router.mjs -+++ b/src/dev-runtime/server/local-api-router.mjs -@@ -356,6 +356,7 @@ const SYSTEM_HEALTH_STORAGE_ACTION_IDS = Object.freeze([ - "storage-read-test-object", - "storage-delete-test-object", - ]); -+const SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID = "storage-expanded-validation"; - const SYSTEM_HEALTH_MANUAL_ACTION_LABELS = Object.freeze({ - "database-check": "Run Database Check", - "full-health-check": "Run Full Health Check", -@@ -4046,43 +4047,75 @@ class ApiRuntimeDataSource { - - storageConnectivityConfigFailure(actionId, storage, scope = "project-prefix") { - const missing = storage.config?.missingKeys?.join(", ") || storage.config?.validationError || "storage configuration incomplete"; -+ const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); - return { - actionId, -+ cleanupStatus: "not-run", -+ durationMs: 0, - environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", - executed: false, - lastChecked: new Date().toISOString(), - message: `Storage connectivity requires configured storage and GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX before this action can run. Missing or invalid: ${missing}.`, -+ operation: action?.operation || "configuration", -+ operationLabel: action?.label || "Storage configuration", -+ permanentObjectCreated: false, - projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), - secretEditingAllowed: false, - secretsExposed: false, - status: "FAIL", - storageStatus: this.ownerStorageStatus(), -+ targetPrefix: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : String(storage.config?.projectsPrefix || "").trim(), - }; - } - -- storageConnectivityResult({ actionId, environmentFolder = "", executed = true, keysListed = 0, message, objectKey, projectsPrefix, status }) { -+ storageConnectivityResult({ -+ action, -+ actionId, -+ cleanupStatus = "not-applicable", -+ durationMs = 0, -+ environmentFolder = "", -+ executed = true, -+ keysListed = 0, -+ message, -+ objectKey, -+ permanentObjectCreated = false, -+ projectsPrefix, -+ status, -+ targetPrefix = "", -+ }) { -+ const resolvedAction = action || STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId) || {}; - return { - actionId, -+ cleanupStatus, -+ durationMs, - environmentFolder, - executed, - keysListed, - lastChecked: new Date().toISOString(), - message, -+ operation: resolvedAction.operation || "", -+ operationLabel: resolvedAction.label || actionId, -+ permanentObjectCreated, - projectsPrefix, - secretEditingAllowed: false, - secretsExposed: false, - status, - storageStatus: this.ownerStorageStatus(), -+ targetPrefix, - testObjectKey: objectKey, - }; - } - - async runStorageConnectivityAction(actionId, options = {}) { -+ if (actionId === SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID) { -+ return this.runStorageExpandedValidation(options); -+ } - const action = STORAGE_CONNECTIVITY_ACTIONS.find((candidate) => candidate.id === actionId); - if (!action) { - throw new Error(`Unknown storage connectivity action: ${actionId || "missing actionId"}.`); - } - -+ const startedAt = Date.now(); - const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; - const storage = createConfiguredProjectAssetStorage(); - const projectsPrefix = String(storage.config?.projectsPrefix || "").trim(); -@@ -4098,7 +4131,9 @@ class ApiRuntimeDataSource { - const result = await storage.listObjects(targetPrefix); - const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; - return this.storageConnectivityResult({ -+ action, - actionId: action.id, -+ durationMs: Date.now() - startedAt, - environmentFolder, - keysListed, - message: result.ok -@@ -4107,6 +4142,7 @@ class ApiRuntimeDataSource { - objectKey, - projectsPrefix, - status: result.ok ? "PASS" : "FAIL", -+ targetPrefix, - }); - } - -@@ -4116,7 +4152,9 @@ class ApiRuntimeDataSource { - : await storage.listProjectObjects(); - const keysListed = Array.isArray(result.keys) ? result.keys.length : 0; - return this.storageConnectivityResult({ -+ action, - actionId: action.id, -+ durationMs: Date.now() - startedAt, - environmentFolder, - keysListed, - message: result.ok -@@ -4125,6 +4163,7 @@ class ApiRuntimeDataSource { - objectKey, - projectsPrefix, - status: result.ok ? "PASS" : "FAIL", -+ targetPrefix, - }); - } - -@@ -4135,14 +4174,19 @@ class ApiRuntimeDataSource { - objectKey, - }); - return this.storageConnectivityResult({ -+ action, - actionId: action.id, -+ cleanupStatus: result.ok ? "requires-delete-action" : "not-created", -+ durationMs: Date.now() - startedAt, - environmentFolder, - message: result.ok - ? `Upload test object completed at ${objectKey}. - : `${result.message || "Storage upload failed."} Verify upload permission for ${targetPrefix}.`, - objectKey, -+ permanentObjectCreated: result.ok === true, - projectsPrefix, - status: result.ok ? "PASS" : "FAIL", -+ targetPrefix, - }); - } - -@@ -4151,7 +4195,9 @@ class ApiRuntimeDataSource { - const text = result.ok ? Buffer.from(result.bytes || []).toString("utf8") : ""; - const contentMatches = text === STORAGE_CONNECTIVITY_TEST_OBJECT_CONTENT; - return this.storageConnectivityResult({ -+ action, - actionId: action.id, -+ durationMs: Date.now() - startedAt, - environmentFolder, - message: result.ok && contentMatches - ? `Read test object completed from ${objectKey}. -@@ -4159,32 +4205,77 @@ class ApiRuntimeDataSource { - objectKey, - projectsPrefix, - status: result.ok && contentMatches ? "PASS" : "FAIL", -+ targetPrefix, - }); - } - - const result = await storage.deleteObject(objectKey); - return this.storageConnectivityResult({ -+ action, - actionId: action.id, -+ cleanupStatus: result.ok ? "completed" : "failed", -+ durationMs: Date.now() - startedAt, - environmentFolder, - message: result.ok - ? `Delete test object completed at ${objectKey}. - : `${result.message || "Storage delete failed."} Verify delete permission for ${targetPrefix}.`, - objectKey, -+ permanentObjectCreated: false, - projectsPrefix, - status: result.ok ? "PASS" : "FAIL", -+ targetPrefix, - }); - } catch (error) { - return this.storageConnectivityResult({ -+ action, - actionId: action.id, -+ durationMs: Date.now() - startedAt, - environmentFolder, - message: `Storage connectivity action failed: ${error instanceof Error ? error.message : String(error || "Unknown storage error.")}`, - objectKey, - projectsPrefix, - status: "FAIL", -+ targetPrefix, - }); - } - } - -+ async runStorageExpandedValidation(options = {}) { -+ const startedAt = Date.now(); -+ const scope = options.scope === "environment-folder" ? "environment-folder" : "project-prefix"; -+ const storage = createConfiguredProjectAssetStorage(); -+ const targetPrefix = this.storageConnectivityTargetPrefix(storage, scope); -+ const objectKey = this.storageConnectivityTestObjectKey(storage, scope); -+ const results = []; -+ for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { -+ results.push(await this.runStorageConnectivityAction(actionId, { scope })); -+ } -+ const uploadResult = results.find((result) => result.actionId === "storage-upload-test-object"); -+ const deleteResult = results.find((result) => result.actionId === "storage-delete-test-object"); -+ const permanentObjectCreated = uploadResult?.status === "PASS" && deleteResult?.status !== "PASS"; -+ const status = overallHealthStatus(results); -+ return { -+ actionId: SYSTEM_HEALTH_STORAGE_EXPANDED_VALIDATION_ACTION_ID, -+ cleanupStatus: permanentObjectCreated ? "failed" : "completed", -+ durationMs: Date.now() - startedAt, -+ environmentFolder: scope === "environment-folder" ? this.storageConnectivityEnvironmentFolder() : "", -+ executed: true, -+ lastChecked: new Date().toISOString(), -+ message: permanentObjectCreated -+ ? `Expanded R2 validation detected a cleanup failure for ${objectKey}; review storage credentials and delete permission. -+ : `Expanded R2 validation completed against ${targetPrefix}; test object cleanup was attempted in the same validation run.`, -+ permanentObjectCreated, -+ projectsPrefix: String(storage.config?.projectsPrefix || "").trim(), -+ secretEditingAllowed: false, -+ secretsExposed: false, -+ status, -+ storageDiagnostics: results, -+ storageStatus: this.ownerStorageStatus(), -+ targetPrefix, -+ testObjectKey: objectKey, -+ }; -+ } ++## Targeted Lane ++ ++`npm run test:service:api-client` ++ ++## Results ++ ++| Command | Result | ++|---|---| ++| `node --check src/api/session-api-client.js` | PASS | ++| `node --check tests/dev-runtime/ServerApiClientStandardization.test.mjs` | PASS | ++| `npm run test:service:api-client` | PASS | ++| `git diff --check` | PASS | ++ ++## Notes ++ ++- The service lane is page/service-level and not named for Team Delta. ++- Full `npm test` was not run because this PR is scoped to API client service coverage. + - ownerPromotionFoundation() { - const steps = [ - { -@@ -4424,20 +4515,20 @@ SELECT pg_database_size(current_database()) AS database_size_bytes, - } - - async adminSystemHealthStorageHealthCheck() { -- const results = []; -- for (const actionId of SYSTEM_HEALTH_STORAGE_ACTION_IDS) { -- results.push(await this.runStorageConnectivityAction(actionId, { scope: "environment-folder" })); -- } -+ const validation = await this.runStorageExpandedValidation({ scope: "environment-folder" }); -+ const results = Array.isArray(validation.storageDiagnostics) ? validation.storageDiagnostics : []; - return { - actionId: "storage-check", - checkedAt: new Date().toISOString(), - label: SYSTEM_HEALTH_MANUAL_ACTION_LABELS["storage-check"], -- message: "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", -+ message: validation.message || "Storage health check executed bucket connectivity, list, upload, read, and delete through the current deployment API.", -+ permanentObjectCreated: validation.permanentObjectCreated === true, - secretEditingAllowed: false, - secretsExposed: false, -- status: overallHealthStatus(results.map((result) => ({ status: result.status }))), -+ status: validation.status || overallHealthStatus(results.map((result) => ({ status: result.status }))), - storageDiagnostics: results, - storageStatus: this.ownerStorageStatus(), -+ validationDurationMs: validation.durationMs, - }; - } - -diff --git a/tests/api/admin-system-health/contract.test.mjs b/tests/api/admin-system-health/contract.test.mjs -index 25aa84b34..54e821f13 100644 ---- a/tests/api/admin-system-health/contract.test.mjs -+++ b/tests/api/admin-system-health/contract.test.mjs -@@ -146,6 +146,25 @@ test("Admin System Health completion contract remains server-owned and current-e - assert.equal(healthText.includes("/uat/projects"), false); - assert.equal(health.secretEditingAllowed, false); - assert.equal(health.secretsExposed, false); -+ const expandedStorage = await apiJson(server.baseUrl, "/api/admin/system-health/storage-connectivity-action", { -+ body: { actionId: "storage-expanded-validation" }, -+ method: "POST", -+ }); -+ assert.deepEqual( -+ expandedStorage.storageDiagnostics.map((row) => row.actionId), -+ [ -+ "storage-bucket-connectivity", -+ "storage-list", -+ "storage-upload-test-object", -+ "storage-read-test-object", -+ "storage-delete-test-object", -+ ], -+ ); -+ assert.equal(expandedStorage.storageDiagnostics.every((row) => row.environmentFolder === "/dev"), true); -+ assert.equal(expandedStorage.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); -+ assert.equal(expandedStorage.permanentObjectCreated, false); -+ assert.equal(JSON.stringify(expandedStorage).includes("api-secret"), false); -+ assert.equal(JSON.stringify(expandedStorage).includes("site-secret"), false); - } finally { - await server.close(); - } -diff --git a/tests/dev-runtime/AdminHealthOperations.test.mjs b/tests/dev-runtime/AdminHealthOperations.test.mjs -index 99fef0757..9ea40988a 100644 ---- a/tests/dev-runtime/AdminHealthOperations.test.mjs -+++ b/tests/dev-runtime/AdminHealthOperations.test.mjs -@@ -311,6 +311,12 @@ test("Admin can view operational health while Creator sessions are blocked", asy - ], - ); - assert.equal(storageAction.storageDiagnostics.every((row) => row.environmentFolder === "/local"), true); -+ assert.equal(storageAction.permanentObjectCreated, false); -+ assert.equal(typeof storageAction.validationDurationMs, "number"); -+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.durationMs === "number"), true); -+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.operationLabel === "string"), true); -+ assert.equal(storageAction.storageDiagnostics.every((row) => typeof row.cleanupStatus === "string"), true); -+ assert.equal(storageAction.storageDiagnostics.every((row) => row.permanentObjectCreated === false), true); - const refreshAction = await apiJson(server.baseUrl, "/api/admin/system-health/action", { - body: { actionId: "refresh" }, - method: "POST", -diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -index 56733ce6a..713344ac0 100644 ---- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -+++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -@@ -233,6 +233,10 @@ test("Admin System Health renders Postgres diagnostics through the safe status A - await expect(page.locator("[data-admin-system-health-storage-value='upload']")).toContainText("/dev"); - await expect(page.locator("[data-admin-system-health-storage-value='read']")).not.toHaveText("Health object"); - await expect(page.locator("[data-admin-system-health-storage-value='delete']")).not.toHaveText("Health object"); -+ await expect(page.locator("[data-admin-system-health-storage-timing='list']")).not.toHaveText("Loading"); -+ await expect(page.locator("[data-admin-system-health-storage-timing='upload']")).not.toHaveText("Loading"); -+ await expect(page.locator("[data-admin-system-health-storage-timing='read']")).not.toHaveText("Loading"); -+ await expect(page.locator("[data-admin-system-health-storage-timing='delete']")).not.toHaveText("Loading"); - await expect(page.getByRole("table", { name: "Runtime health" })).toContainText("Runtime Health"); - await expect(page.locator("[data-admin-system-health-runtime-health-value='environment']")).toHaveText("DEV"); - await expect(page.locator("[data-admin-system-health-runtime-health-value='appVersion']")).toHaveText("1.0.0"); -@@ -286,7 +290,7 @@ test("Admin System Health renders Postgres diagnostics through the safe status A - expect((title || ariaLabel || "").trim()).not.toEqual(""); - } - expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/status"))).toBe(true); -- expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(5); -+ expect(context.requestUrls.filter((url) => url.includes("/api/admin/system-health/storage-connectivity-action"))).toHaveLength(1); - await page.getByRole("button", { name: "Run Runtime Check" }).click(); - await expect(page.getByRole("table", { name: "Manual health action results" })).toContainText("Run Runtime Check"); - expect(context.requestUrls.some((url) => url.includes("/api/admin/system-health/action"))).toBe(true); -@@ -353,5 +357,5 @@ test("Admin System Health operations page keeps scripts and styles external", as - expect(runtimeSource).not.toContain("localStorage"); - expect(runtimeSource).not.toContain("sessionStorage"); - expect(runtimeSource).toContain("runAdminSystemHealthAction"); -- expect(runtimeSource).toContain("runAdminSystemHealthStorageConnectivityAction"); -+ expect(runtimeSource).toContain("runAdminSystemHealthStorageExpandedValidation"); - }); diff --git a/package.json b/package.json index 72c3c9c32..05118574a 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "scripts": { "pretest": "node docs_build/dev/toolbox/checkSharedExtractionGuard.mjs", "test": "node ./scripts/run-node-tests.mjs", + "test:service:api-client": "node ./scripts/run-node-test-files.mjs tests/dev-runtime/ServerApiClientStandardization.test.mjs", "test:audit:locations": "node ./scripts/audit-playwright-test-locations.mjs", "test:playwright:structure": "node ./scripts/audit-playwright-test-locations.mjs", "test:playwright:zero-browser": "node ./scripts/run-targeted-test-lanes.mjs --zero-browser-only", diff --git a/src/api/session-api-client.js b/src/api/session-api-client.js index f31004ea1..3b8c69125 100644 --- a/src/api/session-api-client.js +++ b/src/api/session-api-client.js @@ -51,7 +51,7 @@ export function signIn(options = {}) { } export function logoutSessionUser() { - return unwrap(safeRequestServerApi("/session/logout", { method: "POST" }), "Session logout"); + return requireSessionApiData(safeRequestServerApi("/session/logout", { method: "POST" }), "Session logout"); } export function signOut() { diff --git a/tests/dev-runtime/ServerApiClientStandardization.test.mjs b/tests/dev-runtime/ServerApiClientStandardization.test.mjs index 5530e4074..a694fa78e 100644 --- a/tests/dev-runtime/ServerApiClientStandardization.test.mjs +++ b/tests/dev-runtime/ServerApiClientStandardization.test.mjs @@ -1,10 +1,82 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { clearPublicConfigCache } from "../../src/api/public-config-client.js"; import { + clearServerApiDiagnostics, requireServerApiData, SERVER_DATA_BOUNDARY_RULE, } from "../../src/api/server-api-client.js"; +import { logoutSessionUser } from "../../src/api/session-api-client.js"; + +function installMockServerApi(routes) { + const calls = []; + const previousWindow = globalThis.window; + const previousXmlHttpRequest = globalThis.XMLHttpRequest; + + clearPublicConfigCache(); + clearServerApiDiagnostics(); + + globalThis.window = { + GameFoundryPublicConfig: { + apiUrl: "http://runtime-api.test/api", + }, + }; + + globalThis.XMLHttpRequest = class MockXMLHttpRequest { + constructor() { + this.headers = {}; + this.responseText = ""; + this.status = 0; + } + + open(method, url, async) { + this.method = method; + this.url = url; + this.async = async; + } + + setRequestHeader(name, value) { + this.headers[name] = value; + } + + send(body) { + calls.push({ + body, + headers: { ...this.headers }, + method: this.method, + url: this.url, + }); + const route = routes[`${this.method} ${this.url}`] || { + payload: { + error: `No mock route for ${this.method} ${this.url}`, + ok: false, + }, + status: 404, + }; + this.status = route.status; + this.responseText = JSON.stringify(route.payload); + } + }; + + return { + calls, + restore() { + if (previousWindow === undefined) { + delete globalThis.window; + } else { + globalThis.window = previousWindow; + } + if (previousXmlHttpRequest === undefined) { + delete globalThis.XMLHttpRequest; + } else { + globalThis.XMLHttpRequest = previousXmlHttpRequest; + } + clearPublicConfigCache(); + clearServerApiDiagnostics(); + }, + }; +} test("requireServerApiData returns the server data payload", () => { assert.deepEqual( @@ -57,3 +129,57 @@ test("requireServerApiData supports client-specific restore guidance", () => { /Admin setup status did not return server data\. Restore the admin setup API\./, ); }); + +test("logoutSessionUser uses the standardized session server data boundary", () => { + const mockApi = installMockServerApi({ + "POST http://runtime-api.test/api/session/logout": { + payload: { + data: { + authenticated: false, + userKey: "", + }, + ok: true, + }, + status: 200, + }, + }); + + try { + assert.deepEqual(logoutSessionUser(), { + authenticated: false, + userKey: "", + }); + assert.deepEqual(mockApi.calls, [ + { + body: null, + headers: { + Accept: "application/json", + }, + method: "POST", + url: "http://runtime-api.test/api/session/logout", + }, + ]); + } finally { + mockApi.restore(); + } +}); + +test("logoutSessionUser preserves session restore guidance for missing server data", () => { + const mockApi = installMockServerApi({ + "POST http://runtime-api.test/api/session/logout": { + payload: { + ok: true, + }, + status: 200, + }, + }); + + try { + assert.throws( + () => logoutSessionUser(), + /Session logout did not return server data\. Restore the server auth\/session API\./, + ); + } finally { + mockApi.restore(); + } +});