Files
ss-tools/scripts/full_flow_finance/probe_page242.py
busya bcc69f4bbe feat(dashboard-testing): add sampled traversal and ClickHouse test lab
Add versioned metric graph authority, owned browser evidence, paginated and all-tab traversal, deterministic sampling policies, and analyst-facing run inspection.

Provision the DEV/PREPROD/PROD Superset, Gitea and million-row ClickHouse lab; retain reproducible lifecycle evidence and explicit incomplete-traversal limits.
2026-10-02 10:54:42 +03:00

201 lines
11 KiB
Python

#!/usr/bin/env python3
# #region FullFlow.FinancePage242Probe [C:4] [TYPE Module] [SEMANTICS fixture,readonly,browser,pagination,diagnostic]
# @BRIEF Reproduce actual exact-chart navigation through page242 with production reader stage observations.
# @INVARIANT Read-only UI/query observations; no public run, lease, receipt, data mutation or completeness result is manufactured.
import argparse
import asyncio
import json
import os
from pathlib import Path
import sys
import time
from hashlib import sha256
from playwright.async_api import async_playwright
from release import private_environment
from browser_memory import sample
from browser_reconstruct import reconstruct
sys.path.insert(0,str(Path(__file__).resolve().parents[2]/'backend'))
from src.services.dashboard_testing.execution.providers.browser_pagination import SupersetPageReader # noqa: E402
from src.services.dashboard_testing.execution.providers.browser_traversal_inputs import PaginationTraversalInput # noqa: E402
from src.services.dashboard_testing.execution.providers.browser_pagination_response import page_query_context # noqa: E402
from browser_page_replace import replace # noqa: E402
# #region FullFlow.FinancePage242Probe.ContextHashes [C:3] [TYPE Function]
# @POST Semantic field identities are retained as hashes; arbitrary query/filter values and credentials are excluded.
def context_hashes(value, prefix=''):
if isinstance(value,dict):
return {path:digest for key,item in value.items() for path,digest in context_hashes(item,f'{prefix}.{key}').items()}
if isinstance(value,list) and value:
return {path:digest for index,item in enumerate(value) for path,digest in context_hashes(item,f'{prefix}[{index}]').items()}
return {prefix:sha256(json.dumps(value,sort_keys=True).encode()).hexdigest()}
# #endregion FullFlow.FinancePage242Probe.ContextHashes
# #region FullFlow.FinancePage242Probe.Retain [C:1] [TYPE Function]
# @POST Only safe bounded observations are retained; no authentication values or arbitrary exception text is printed.
def retain(output, value):
with (output/'observations.jsonl').open('a') as stream:
stream.write(json.dumps(value,ensure_ascii=False)+'\n')
# #endregion FullFlow.FinancePage242Probe.Retain
# #region FullFlow.FinancePage242Probe.GarbageCollection [C:2] [TYPE Function]
# @POST Before/after numeric samples distinguish collectible garbage from retained renderer memory without changing dashboard state.
# @RATIONALE Actual renderer heap/DOM grew rapidly despite constant bounded page payloads; an explicit collection is a causal diagnostic before selecting a runtime policy.
# @REJECTED Increasing page timeout or assuming a reachable frontend leak from pre-collection heap growth would not establish the cause.
async def collect_garbage(reader, args, cdp, ordinal):
before = await sample(cdp)
started = time.monotonic()
collected = False
try:
await asyncio.wait_for(reader.page.request_gc(),timeout=10)
collected = True
except Exception:
pass
retain(args.output,{'scope':'diagnostic only','ordinal':ordinal,'garbage_collection':{
'completed':collected,'elapsed_seconds':round(time.monotonic()-started,3),
'before':before,'after':await sample(cdp)}})
# #endregion FullFlow.FinancePage242Probe.GarbageCollection
# #region FullFlow.FinancePage242Probe.Renew [C:2] [TYPE Function]
# @POST Real document renewal is measured with source-derived diagnostic coordinates; no owned-run frontier is created.
async def renew_document(reader, args, cdp, ordinal):
source = {key:reader.exchange[key] for key in ('source_total','dataset_id','context_digest')}
source.update(chart_id=reader.limits.chart_id,page_size=reader.limits.page_size)
before = await sample(cdp)
started = time.monotonic()
old_hashes = dict(reader.context_hashes)
try:
if args.renew_mode == 'new-page':
await asyncio.wait_for(replace(reader,ordinal+1,source),timeout=120)
try:
await cdp.detach()
except Exception:
pass
cdp = await reader.page.context.new_cdp_session(reader.page)
else:
await asyncio.wait_for(reader.maintain(ordinal+1,{'source':source}),timeout=120)
except Exception:
retain(args.output,{'scope':'diagnostic only','ordinal':ordinal,'renewal_failure':{
'original_source':source,'fresh_source':{key:reader.exchange.get(key) for key in source} if reader.exchange else None,
'changed_context_paths':[key for key in sorted(old_hashes.keys() | reader.context_hashes.keys())
if old_hashes.get(key) != reader.context_hashes.get(key)],
'stage':reader.stage,'browser_position':reader.position}})
raise
retain(args.output,{'scope':'diagnostic only','ordinal':ordinal,'document_renewal':{
'mode':args.renew_mode,'elapsed_seconds':round(time.monotonic()-started,3),
'open_page_count':len(reader.page.context.pages),'before':before,'after':await sample(cdp)}})
return cdp
# #endregion FullFlow.FinancePage242Probe.Renew
# #region FullFlow.FinancePage242Probe.Walk [C:3] [TYPE Function]
# @POST Genuine server response/context/offset and ordered key boundaries are observed separately for each UI page.
async def walk(reader, args, cdp):
await asyncio.wait_for(reader.prepare(),timeout=120)
first = 1
if args.reconstruct_target:
await asyncio.wait_for(reconstruct(reader,args.reconstruct_target,
lambda value:retain(args.output,value)),timeout=120)
first = args.reconstruct_target
for ordinal in range(first,args.through_page+1):
started = time.monotonic()
try:
observed = await asyncio.wait_for(reader.read_page(ordinal,timeout_seconds=70),timeout=70)
except Exception as exc:
code = str(exc) if isinstance(exc,ValueError) and str(exc).startswith('BROWSER_') else 'BROWSER_TRAVERSAL_PAGE_TIMEOUT'
diagnostic = reader.diagnostic_state(code)
try:
diagnostic = await asyncio.wait_for(reader.diagnostic_snapshot(code),timeout=2)
except Exception:
pass
retain(args.output,{'scope':'diagnostic only','diagnostic':diagnostic,
'elapsed_seconds':round(time.monotonic()-started,3),'memory':await sample(cdp)})
return False
retain(args.output,{'scope':'diagnostic only','ordinal':ordinal,'row_count':len(observed['rows']),
'source_total':observed['source_total'],'row_offset':observed['row_offset'],
'first_key':observed['ordering_keys'][0] if observed['ordering_keys'] else None,
'last_key':observed['ordering_keys'][-1] if observed['ordering_keys'] else None,
'context_digest':observed['context_digest'],'response_sha256':observed['response_sha256'],
'elapsed_seconds':round(time.monotonic()-started,3),
'request_byte_length':reader.last_request_byte_length,'response_byte_length':reader.last_response_byte_length})
if ordinal % 10 == 0 or ordinal == 1:
retain(args.output,{'scope':'diagnostic only','ordinal':ordinal,'memory':await sample(cdp)})
if ordinal in args.gc_at:
await collect_garbage(reader,args,cdp,ordinal)
if ordinal in args.renew_at and ordinal < args.through_page:
cdp = await renew_document(reader,args,cdp,ordinal)
return True
# #endregion FullFlow.FinancePage242Probe.Walk
# #region FullFlow.FinancePage242Probe.Run [C:3] [TYPE Function]
# @PRE Original fixture restored and frozen; fresh output/private env and authenticated local DEV user.
async def run(args):
private_environment(args.env_file)
args.output.mkdir(parents=True,exist_ok=False)
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context(viewport={'width':1600,'height':1000})
page = await context.new_page()
try:
await page.goto('http://127.0.0.1:18111/login/',wait_until='domcontentloaded')
await page.locator('#username').fill('admin')
await page.locator('#password').fill(os.environ['FIXTURE_PASSWORD'])
await page.locator('input[type="submit"],button[type="submit"]').click()
await page.wait_for_url(lambda url:'/login' not in url,timeout=30000)
await page.goto('http://127.0.0.1:18111/superset/dashboard/2/',wait_until='domcontentloaded')
await page.locator('#chart-id-6 tbody tr').first.wait_for(state='visible',timeout=90000)
limits = PaginationTraversalInput(chart_id=6,table_selector='table:has(tbody tr)',
header_selector='table:has(thead th)',pagination_selector='.pagination',current_page_selector='li.active a',
next_page_selector='a[role="button"][href="#page-{next_page_index}"]',first_page_selector='a[href="#page-0"]',
ordering_column='counterparty_id',page_size=200)
cdp = await page.context.new_cdp_session(page)
reader = SupersetPageReader(page,limits)
reader.context_hashes = {}
# #region FullFlow.FinancePage242Probe.Run.RequestContext [C:2] [TYPE Function]
# @POST Only exact chart semantic hashes are observed; no request bytes persist.
def observe_context(request):
if reader.chart_request(request):
reader.context_hashes = context_hashes(page_query_context(request.post_data_json))
# #endregion FullFlow.FinancePage242Probe.Run.RequestContext
reader.context_observer = observe_context
page.on('request',observe_context)
success = await walk(reader,args,cdp)
return 0 if success else 1
finally:
await browser.close()
# #endregion FullFlow.FinancePage242Probe.Run
# #region FullFlow.FinancePage242Probe.Main [C:2] [TYPE Function]
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--env-file',type=Path,required=True)
parser.add_argument('--output',type=Path,required=True)
parser.add_argument('--through-page',type=int,default=242)
parser.add_argument('--gc-at',type=int,nargs='*',default=[])
parser.add_argument('--reconstruct-target',type=int,default=0)
parser.add_argument('--renew-at',type=int,nargs='*',default=[])
parser.add_argument('--renew-mode',choices=['document','new-page'],default='document')
args = parser.parse_args()
if not 1 <= args.through_page <= 625:
raise SystemExit('Bounded diagnostic page required')
if any(not 1 <= ordinal <= args.through_page for ordinal in args.gc_at):
raise SystemExit('Collection page must be within diagnostic scope')
if args.reconstruct_target and not 1 <= args.reconstruct_target <= args.through_page:
raise SystemExit('Reconstruction target must be within diagnostic scope')
if any(not 1 <= ordinal < args.through_page for ordinal in args.renew_at):
raise SystemExit('Renewal page must precede diagnostic end')
return asyncio.run(run(args))
# #endregion FullFlow.FinancePage242Probe.Main
if __name__ == '__main__':
raise SystemExit(main())
# #endregion FullFlow.FinancePage242Probe