16 Commits
Author SHA1 Message Date
tejasshasane 48d0d786b2 Add DAC and Intraday token setup support 2026-11-02 12:46:15 +05:30
tejasshasane d681002b0f Merge pull request 'Add manual and offset time modes' (#10) from try-captcha-automation into development
Reviewed-on: #10
2026-11-02 06:35:38 +01:00
tejasshasane eb8a831edb Add manual and offset time modes 2026-10-13 09:45:51 +05:30
tejasshasane 2454ca3180 Merge pull request 'Try captcha automation' (#9) from try-captcha-automation into development
Reviewed-on: #9
2026-10-13 06:01:34 +02:00
tejasshasane 771b755ca1 Remove unused UPC time session settings 2026-10-13 09:29:50 +05:30
tejasshasane d9218709f0 session token fix for captcha 2026-10-09 12:39:32 +05:30
tejasshasane 385a9e22cc Update requirements.txt 2026-10-08 17:57:58 +05:30
tejasshasane 24f2c1fe6a Add CAPTCHA automation beta flow 2026-10-08 17:36:30 +05:30
tejasshasane 631f64b84f Merge pull request 'Fix REC token creation and time session flow' (#8) from development into master
Reviewed-on: #8
2026-10-06 13:34:19 +02:00
tejasshasane c42cc83aa1 Fix REC token creation and time session flow 2026-10-06 17:01:26 +05:30
tejasshasane d6c1f7a3d3 Merge pull request 'Add shared configuration template' (#7) from development into master
Reviewed-on: #7
2026-10-06 10:10:36 +02:00
tejasshasane 106fc29c6a Add shared configuration template 2026-10-06 13:32:09 +05:30
tejasshasane 0312a542a2 Merge pull request 'Fix UPC token-only variant selection' (#6) from development into master
Reviewed-on: #6
2026-10-06 07:14:33 +02:00
tejasshasane 4218f8572d Fix UPC token-only variant selection 2026-10-06 10:36:02 +05:30
tejasshasane d89ebdf869 Merge pull request 'Archify' (#5) from development into master
Reviewed-on: #5
2026-09-29 14:09:30 +02:00
tejasshasane f0496b3d67 Archify 2026-09-29 17:38:15 +05:30
34 changed files with 50837 additions and 245 deletions
@@ -0,0 +1,395 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "PXIL Automation Runtime",
"output": ".archify/architecture-pxil-runtime-20260929-151855/pxil-runtime.html",
"quality_profile": "showcase",
"locale": "en",
"repository": {
"url": "https://git.tejasshasane.xyz/tejasshasane/pxil-selenium-automation.git",
"revision": "5ab89ce0059525b31558a1fa82251a2a6f610dd7",
"link_mode": "local-only"
}
},
"components": [
{
"id": "runner",
"type": "backend",
"label": "pytest runner",
"sublabel": "Selection + fixture lifecycle",
"pos": [
60,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/conftest.py",
"line": 37,
"end_line": 62
},
{
"path": "pytest.ini",
"line": 1,
"end_line": 14
}
]
},
{
"id": "flows",
"type": "backend",
"label": "Automation flows",
"sublabel": "Setup / bidding / matching",
"pos": [
360,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 54,
"end_line": 90
},
{
"path": "tests/regression/exchange/idas/matching/test_idas_exchange_matching_flow.py",
"line": 76,
"end_line": 156
},
{
"path": "tests/setup/test_combined_setup.py",
"line": 13,
"end_line": 37
}
]
},
{
"id": "browser",
"type": "frontend",
"label": "Chrome + WebDriver",
"sublabel": "Selenium browser dependency",
"pos": [
680,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/browser.py",
"line": 1,
"end_line": 14
},
{
"path": "tests/regression/auth.py",
"line": 46,
"end_line": 86
}
],
"tag": "Manual CAPTCHA for Pratyay"
},
{
"id": "pratyay",
"type": "external",
"label": "Pratyay portal",
"sublabel": "Exchange / member UI",
"pos": [
970,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 68,
"end_line": 90
},
{
"path": "tests/regression/exchange/idas/matching/test_idas_exchange_matching_flow.py",
"line": 47,
"end_line": 59
}
]
},
{
"id": "config",
"type": "database",
"label": "Configuration",
"sublabel": "JSON + role credentials",
"pos": [
360,
100
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/config_reader.py",
"line": 6,
"end_line": 32
},
{
"path": "tests/regression/config_reader.py",
"line": 5,
"end_line": 29
},
{
"path": "tests/regression/config_reader.py",
"line": 121,
"end_line": 150
}
]
},
{
"id": "inputs",
"type": "database",
"label": "Bid input files",
"sublabel": "Excel / JSON / upload folders",
"pos": [
360,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/input_manifest.py",
"line": 4,
"end_line": 28
},
{
"path": "tests/regression/exchange/idas/bidding/json_reader.py",
"line": 5,
"end_line": 35
},
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 217,
"end_line": 249
}
]
},
{
"id": "reporting",
"type": "backend",
"label": "Evidence reporting",
"sublabel": "Step capture + pytest hooks",
"pos": [
60,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/step_actions.py",
"line": 10,
"end_line": 40
},
{
"path": "tests/conftest.py",
"line": 104,
"end_line": 175
}
]
},
{
"id": "artifacts",
"type": "database",
"label": "Run artifacts",
"sublabel": "HTML / screenshots / logs",
"pos": [
60,
770
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/conftest.py",
"line": 37,
"end_line": 48
},
{
"path": "tests/conftest.py",
"line": 70,
"end_line": 97
},
{
"path": "tests/step_actions.py",
"line": 21,
"end_line": 35
}
]
},
{
"id": "mco",
"type": "external",
"label": "MCO portal",
"sublabel": "Independent login-only test",
"pos": [
970,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/matching/test_idas_mco_login.py",
"line": 13,
"end_line": 36
}
],
"tag": "Only when enabled"
}
],
"boundaries": [
{
"kind": "region",
"label": "LOCAL AUTOMATION HOST | trusted code, inputs and credentials",
"wraps": [
"runner",
"flows",
"browser",
"config",
"inputs",
"reporting",
"artifacts"
],
"pad": 28
},
{
"kind": "security-group",
"label": "Browser process boundary",
"wraps": [
"browser"
],
"pad": 18
},
{
"kind": "region",
"label": "REMOTE PORTALS | separate authentication",
"wraps": [
"pratyay",
"mco"
],
"pad": 28
}
],
"connections": [
{
"id": "selected-tests",
"from": "runner",
"to": "flows",
"label": "run tests",
"variant": "emphasis"
},
{
"id": "ui-actions",
"from": "flows",
"to": "browser",
"label": "UI commands",
"variant": "emphasis"
},
{
"id": "portal-session",
"from": "browser",
"to": "pratyay",
"label": "login + UI",
"variant": "emphasis"
},
{
"id": "read-settings",
"from": "flows",
"to": "config",
"label": "read settings"
},
{
"id": "read-bids",
"from": "flows",
"to": "inputs",
"label": "read bids"
},
{
"id": "report-hooks",
"from": "runner",
"to": "reporting",
"label": "test hooks"
},
{
"id": "write-evidence",
"from": "reporting",
"to": "artifacts",
"label": "write evidence"
},
{
"id": "optional-mco",
"from": "browser",
"to": "mco",
"label": "if enabled: login",
"variant": "dashed"
}
],
"cards": [
{
"dot": "cyan",
"title": "Primary path",
"items": [
"pytest runs selected tests; flow code controls Selenium and Chrome, which interacts with the remote Pratyay UI.",
"Pratyay login fills credentials and pauses for manual CAPTCHA entry before submission (tests/regression/auth.py:46-122).",
"Arrows describe control and I/O; results return to pytest. No direct backend API or database integration is asserted."
]
},
{
"dot": "violet",
"title": "Flow selection and capabilities",
"items": [
"Root config.json selects setup products and session/token actions; config/runtime.json selects regression environment, role, product and flow.",
"Bidding supports Excel/JSON grids, files and folders. Optional parallel folder workers create their own browsers (bidding flow:551-587).",
"Matching requires the exchange role; rejected or unsupported bid-transfer status stops matching (matching flow:28-44, 123-135)."
]
},
{
"dot": "rose",
"title": "Trust and external dependencies",
"items": [
"Boundaries show host/process and remote ownership, not verified network isolation or firewall policy.",
"Credentials come from local JSON and are entered into remote login forms. Values and endpoint addresses are omitted.",
"Chrome/WebDriver is an external runtime dependency. Pratyay and MCO server internals and transport guarantees are outside the observed source.",
"MCO is an independent, enabled-only dashboard login test; no Pratyay-to-MCO backend connection is inferred."
]
},
{
"dot": "emerald",
"title": "Evidence and scope",
"items": [
"StepActions requests browser screenshots; pytest hooks attach evidence and configure pytest-html. Reports live under reports/.",
"Matching configures browser downloads separately; setup can save session backups. These details are kept off the main path.",
"Snapshot: clean repository at 5ab89ce0059525b31558a1fa82251a2a6f610dd7. Sources use local-only links.",
"Registered reports/order-book/trade-book markers are placeholders, not implemented runtime services (RUN_COMMANDS.md)."
]
}
]
}
@@ -0,0 +1,721 @@
{
"schemaVersion": 1,
"ok": true,
"command": "browser-check",
"evidenceKind": "automated-browser",
"status": "pass",
"visualReview": "not-requested",
"provenance": "current",
"deliveryReceiptId": "95e7d275-4f11-44f8-af68-086f2d8272bd",
"artifact": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.html",
"sha256": "3db6aa51a98c38d8aebafa9dd0682f3ea5c0ffd1d554c2ac6f000c94b73a193a",
"bytes": 767845
},
"state": {
"detail": "read",
"motion": "still"
},
"chrome": {
"status": "available",
"executable": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
},
"diagnostics": [],
"containment": {
"status": "pass",
"policy": "fit-or-reader-declared-readable-vertical-scroll",
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 920,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 777,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1058,
"diagramWidth": 1028,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 780,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.697068403908795,
"minimumProjectedNonEdgeTextPx": 7.534201954397394,
"minimumProjectedEdgeTextPx": 6.697068403908795,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1163,
"diagramWidth": 1133,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.381107491856677,
"minimumProjectedNonEdgeTextPx": 8.303745928338762,
"minimumProjectedEdgeTextPx": 7.381107491856677,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1476,
"diagramWidth": 1426,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"themeStates": {
"status": "pass",
"viewports": [
{
"width": 1440,
"height": 900,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1600,
"height": 1000,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1920,
"height": 1080,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 2048,
"height": 1320,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1440,
"height": 900,
"requestedTheme": "dark",
"resolvedTheme": "dark",
"ok": true
},
{
"width": 2048,
"height": 1320,
"requestedTheme": "dark",
"resolvedTheme": "dark",
"ok": true
}
]
},
"readability": {
"status": "pass",
"minimumProjectedNodeTextPx": 6,
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 920,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 777,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1058,
"diagramWidth": 1028,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 780,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.697068403908795,
"minimumProjectedNonEdgeTextPx": 7.534201954397394,
"minimumProjectedEdgeTextPx": 6.697068403908795,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1163,
"diagramWidth": 1133,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.381107491856677,
"minimumProjectedNonEdgeTextPx": 8.303745928338762,
"minimumProjectedEdgeTextPx": 7.381107491856677,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1476,
"diagramWidth": 1426,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"viewerChrome": {
"status": "pass",
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 920,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 777,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1058,
"diagramWidth": 1028,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 780,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.697068403908795,
"minimumProjectedNonEdgeTextPx": 7.534201954397394,
"minimumProjectedEdgeTextPx": 6.697068403908795,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1163,
"diagramWidth": 1133,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.381107491856677,
"minimumProjectedNonEdgeTextPx": 8.303745928338762,
"minimumProjectedEdgeTextPx": 7.381107491856677,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1476,
"diagramWidth": 1426,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"captures": {
"status": "not-requested",
"screenshots": [],
"contactSheet": null
},
"sidecars": {
"receipt": "pxil-runtime.browser-check.json",
"files": []
}
}
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"receiptId": "95e7d275-4f11-44f8-af68-086f2d8272bd",
"status": "current",
"command": "deliver",
"type": "architecture",
"input": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\candidate.json",
"output": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.html",
"specification": {
"sha256": "5255f87576eeb3afd6713d933790d481cf9e8034f24453367adc240aa958ccbc",
"bytes": 20698
},
"artifact": {
"sha256": "3db6aa51a98c38d8aebafa9dd0682f3ea5c0ffd1d554c2ac6f000c94b73a193a",
"bytes": 767845
}
}
@@ -0,0 +1,47 @@
{
"schemaVersion": 1,
"ok": true,
"command": "finalize",
"status": "pass",
"type": "architecture",
"quality": "showcase",
"specification": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\candidate.json",
"sha256": "5255f87576eeb3afd6713d933790d481cf9e8034f24453367adc240aa958ccbc",
"bytes": 20698
},
"artifact": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.html",
"sha256": "3db6aa51a98c38d8aebafa9dd0682f3ea5c0ffd1d554c2ac6f000c94b73a193a",
"bytes": 767845
},
"gates": {
"validate": "pass",
"deliver": "pass",
"check": "pass",
"browser-check": "pass"
},
"diagnostics": [],
"diagnosticSummary": {
"total": 0,
"shown": 0,
"truncated": false
},
"evidence": {
"receipt": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.finalize.json",
"summaryReceipt": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.finalize-summary.json",
"browserCheckReceipt": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.browser-check.json"
},
"update": {
"status": "current",
"installedVersion": "3.0.1",
"availableVersion": "3.0.1",
"releaseNotes": null,
"checkedAt": "2026-09-29T09:50:05.829Z",
"source": "cache",
"noticeRequired": false,
"noticeText": null
},
"visualReview": "not-requested",
"durationMs": 10625
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,461 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "PXIL Runtime: Sessions, Tokens and Regression",
"output": ".archify/architecture-pxil-setup-runtime-20260929-155341/pxil-setup-runtime.html",
"quality_profile": "showcase",
"locale": "en",
"repository": {
"url": "https://git.tejasshasane.xyz/tejasshasane/pxil-selenium-automation.git",
"revision": "5ab89ce0059525b31558a1fa82251a2a6f610dd7",
"link_mode": "local-only"
}
},
"components": [
{
"id": "runner",
"type": "backend",
"label": "pytest runner",
"sublabel": "Selection + fixture lifecycle",
"pos": [
60,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/conftest.py",
"line": 37,
"end_line": 62
},
{
"path": "pytest.ini",
"line": 1,
"end_line": 14
}
]
},
{
"id": "flows",
"type": "backend",
"label": "Automation flows",
"sublabel": "Setup / bidding / matching",
"pos": [
360,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 54,
"end_line": 90
},
{
"path": "tests/regression/exchange/idas/matching/test_idas_exchange_matching_flow.py",
"line": 76,
"end_line": 156
},
{
"path": "tests/setup/test_combined_setup.py",
"line": 13,
"end_line": 37
}
]
},
{
"id": "browser",
"type": "frontend",
"label": "Chrome + WebDriver",
"sublabel": "Selenium browser dependency",
"pos": [
680,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/browser.py",
"line": 1,
"end_line": 14
},
{
"path": "tests/regression/auth.py",
"line": 46,
"end_line": 86
}
],
"tag": "Manual CAPTCHA for Pratyay"
},
{
"id": "pratyay",
"type": "external",
"label": "Pratyay portal",
"sublabel": "Exchange / member UI",
"pos": [
970,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 68,
"end_line": 90
},
{
"path": "tests/regression/exchange/idas/matching/test_idas_exchange_matching_flow.py",
"line": 47,
"end_line": 59
}
]
},
{
"id": "config",
"type": "database",
"label": "Config + bid inputs",
"sublabel": "JSON / Excel / upload folders",
"pos": [
360,
100
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/config_reader.py",
"line": 6,
"end_line": 32
},
{
"path": "tests/regression/config_reader.py",
"line": 5,
"end_line": 29
},
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 217,
"end_line": 249
}
]
},
{
"id": "reporting",
"type": "backend",
"label": "Evidence reporting",
"sublabel": "Step capture + pytest hooks",
"pos": [
60,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/step_actions.py",
"line": 10,
"end_line": 40
},
{
"path": "tests/conftest.py",
"line": 104,
"end_line": 175
}
]
},
{
"id": "artifacts",
"type": "database",
"label": "Run artifacts",
"sublabel": "HTML / screenshots / logs",
"pos": [
60,
770
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/conftest.py",
"line": 37,
"end_line": 48
},
{
"path": "tests/conftest.py",
"line": 70,
"end_line": 97
},
{
"path": "tests/step_actions.py",
"line": 21,
"end_line": 35
}
]
},
{
"id": "mco",
"type": "external",
"label": "MCO portal",
"sublabel": "Independent login-only test",
"pos": [
970,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/matching/test_idas_mco_login.py",
"line": 13,
"end_line": 36
}
],
"tag": "Only when enabled"
},
{
"id": "sessions",
"type": "backend",
"label": "Trading session setup",
"sublabel": "ME instance + times + Save",
"tag": "When run_session = true",
"pos": [
360,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/setup/sessions/test_session.py",
"line": 24,
"end_line": 64
},
{
"path": "tests/setup/test_combined_setup.py",
"line": 21,
"end_line": 37
}
]
},
{
"id": "tokens",
"type": "backend",
"label": "Trading token setup",
"sublabel": "Per product code + Save",
"tag": "When run_token = true",
"pos": [
360,
800
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 150,
"end_line": 180
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 212,
"end_line": 326
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 328,
"end_line": 342
}
]
}
],
"boundaries": [
{
"kind": "region",
"label": "LOCAL AUTOMATION HOST | trusted code, inputs and credentials",
"wraps": [
"runner",
"flows",
"browser",
"config",
"reporting",
"artifacts",
"sessions",
"tokens"
],
"pad": 28
},
{
"kind": "security-group",
"label": "Browser process boundary",
"wraps": [
"browser"
],
"pad": 18
},
{
"kind": "region",
"label": "REMOTE PORTALS | separate authentication",
"wraps": [
"pratyay",
"mco"
],
"pad": 28
}
],
"connections": [
{
"id": "selected-tests",
"from": "runner",
"to": "flows",
"label": "run tests",
"variant": "emphasis"
},
{
"id": "ui-actions",
"from": "flows",
"to": "browser",
"label": "UI commands",
"variant": "emphasis"
},
{
"id": "portal-session",
"from": "browser",
"to": "pratyay",
"label": "login + UI",
"variant": "emphasis"
},
{
"id": "read-settings",
"from": "flows",
"to": "config",
"label": "read files"
},
{
"id": "report-hooks",
"from": "runner",
"to": "reporting",
"label": "test hooks"
},
{
"id": "write-evidence",
"from": "reporting",
"to": "artifacts",
"label": "write evidence"
},
{
"id": "optional-mco",
"from": "browser",
"to": "mco",
"label": "if enabled: login",
"variant": "dashed"
},
{
"id": "session-enabled",
"from": "flows",
"to": "sessions",
"label": "run_session",
"variant": "default"
},
{
"id": "combined-order",
"from": "sessions",
"to": "tokens",
"label": "both enabled: next",
"variant": "dashed"
},
{
"id": "token-only",
"from": "flows",
"to": "tokens",
"label": "token only",
"variant": "default",
"fromSide": "bottom",
"toSide": "right"
}
],
"cards": [
{
"dot": "amber",
"title": "Session and token execution",
"items": [
"After setup login and manual CAPTCHA: session-only runs create_session; token-only runs run_token_setup; combined runs create_session then run_token_setup for each product code.",
"Both setup modules use the shared Selenium driver and StepActions through Automation flows. The dashed session-to-token arrow denotes controller ordering, not a direct module call (tests/setup/test_combined_setup.py:21-37).",
"Session creation opens Trading Session, selects the ME instance, fills start/end times, then clicks Save (tests/setup/sessions/test_session.py:24-40).",
"These are trading sessions and trading tokens, distinct from browser authentication/session backups."
]
},
{
"dot": "orange",
"title": "Product-specific token behavior",
"items": [
"IDAS/default branch: create token, set delivery dates and expiry, Save, edit ORDER_NOR / ORDER_EXCH / TRADE times, then Save and Activate (tests/setup/tokens/test_trading_token.py:150-180, 264-325).",
"RTM branch fills delivery and session times and saves the token; activation is not called (lines 212-225, 254-261).",
"UPC branch fills configured order/trade times, saves, then resets the generator (lines 228-258).",
"config.json selects enabled products and run_session/run_token flags. Disabled products and both-false products are omitted; UPC variants have additional routing rules (tests/config_reader.py:36-83)."
]
},
{
"dot": "cyan",
"title": "Primary path",
"items": [
"pytest runs selected tests; flow code controls Selenium and Chrome, which interacts with the remote Pratyay UI.",
"Pratyay login fills credentials and pauses for manual CAPTCHA entry before submission (tests/regression/auth.py:46-122).",
"Arrows describe control and I/O; results return to pytest. No direct backend API or database integration is asserted."
]
},
{
"dot": "violet",
"title": "Flow selection and capabilities",
"items": [
"Root config.json selects setup products and session/token actions; config/runtime.json selects regression environment, role, product and flow.",
"Bidding supports Excel/JSON grids, files and folders. Optional parallel folder workers create their own browsers (bidding flow:551-587).",
"Matching requires the exchange role; rejected or unsupported bid-transfer status stops matching (matching flow:28-44, 123-135)."
]
},
{
"dot": "rose",
"title": "Trust and external dependencies",
"items": [
"Boundaries show host/process and remote ownership, not verified network isolation or firewall policy.",
"Credentials come from local JSON and are entered into remote login forms. Values and endpoint addresses are omitted.",
"Chrome/WebDriver is an external runtime dependency. Pratyay and MCO server internals and transport guarantees are outside the observed source.",
"MCO is an independent, enabled-only dashboard login test; no Pratyay-to-MCO backend connection is inferred."
]
},
{
"dot": "emerald",
"title": "Evidence and scope",
"items": [
"StepActions requests browser screenshots; pytest hooks attach evidence and configure pytest-html. Reports live under reports/.",
"Matching configures browser downloads separately; setup can save session backups. These details are kept off the main path.",
"Source snapshot: 5ab89ce0059525b31558a1fa82251a2a6f610dd7; tracked code unchanged. Only generated .archify/ artifacts are untracked. Sources use local-only links.",
"Registered reports/order-book/trade-book markers are placeholders, not implemented runtime services (RUN_COMMANDS.md)."
]
}
]
}
@@ -0,0 +1,721 @@
{
"schemaVersion": 1,
"ok": true,
"command": "browser-check",
"evidenceKind": "automated-browser",
"status": "pass",
"visualReview": "not-requested",
"provenance": "current",
"deliveryReceiptId": "4a5fda98-da80-480c-a108-3987d32a0fb2",
"artifact": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.html",
"sha256": "7f1a2d9fcc2ed49fc6a2c2c9c541bd0620ab0ca1cf04b5e388dae27d01e5b3ec",
"bytes": 773779
},
"state": {
"detail": "read",
"motion": "still"
},
"chrome": {
"status": "available",
"executable": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
},
"diagnostics": [],
"containment": {
"status": "pass",
"policy": "fit-or-reader-declared-readable-vertical-scroll",
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 945,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1011,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1128,
"diagramWidth": 1098,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.153094462540716,
"minimumProjectedNonEdgeTextPx": 8.047231270358306,
"minimumProjectedEdgeTextPx": 7.153094462540716,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1431,
"diagramWidth": 1381,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"themeStates": {
"status": "pass",
"viewports": [
{
"width": 1440,
"height": 900,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1600,
"height": 1000,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1920,
"height": 1080,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 2048,
"height": 1320,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1440,
"height": 900,
"requestedTheme": "dark",
"resolvedTheme": "dark",
"ok": true
},
{
"width": 2048,
"height": 1320,
"requestedTheme": "dark",
"resolvedTheme": "dark",
"ok": true
}
]
},
"readability": {
"status": "pass",
"minimumProjectedNodeTextPx": 6,
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 945,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1011,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1128,
"diagramWidth": 1098,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.153094462540716,
"minimumProjectedNonEdgeTextPx": 8.047231270358306,
"minimumProjectedEdgeTextPx": 7.153094462540716,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1431,
"diagramWidth": 1381,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"viewerChrome": {
"status": "pass",
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 945,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1011,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1128,
"diagramWidth": 1098,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.153094462540716,
"minimumProjectedNonEdgeTextPx": 8.047231270358306,
"minimumProjectedEdgeTextPx": 7.153094462540716,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1431,
"diagramWidth": 1381,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"captures": {
"status": "not-requested",
"screenshots": [],
"contactSheet": null
},
"sidecars": {
"receipt": "pxil-setup-runtime.browser-check.json",
"files": []
}
}
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"receiptId": "4a5fda98-da80-480c-a108-3987d32a0fb2",
"status": "current",
"command": "deliver",
"type": "architecture",
"input": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\candidate.json",
"output": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.html",
"specification": {
"sha256": "1419cbca3fed61e28342a8b9767aa43fa85a7b18ff380e93ebedccfa96f9549b",
"bytes": 25091
},
"artifact": {
"sha256": "7f1a2d9fcc2ed49fc6a2c2c9c541bd0620ab0ca1cf04b5e388dae27d01e5b3ec",
"bytes": 773779
}
}
@@ -0,0 +1,85 @@
{
"schemaVersion": 1,
"ok": true,
"command": "finalize",
"status": "pass",
"type": "architecture",
"quality": "showcase",
"specification": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\candidate.json",
"sha256": "1419cbca3fed61e28342a8b9767aa43fa85a7b18ff380e93ebedccfa96f9549b",
"bytes": 25091
},
"artifact": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.html",
"sha256": "7f1a2d9fcc2ed49fc6a2c2c9c541bd0620ab0ca1cf04b5e388dae27d01e5b3ec",
"bytes": 773779
},
"gates": {
"validate": "pass",
"deliver": "pass",
"check": "pass",
"browser-check": "pass"
},
"diagnostics": [],
"diagnosticSummary": {
"total": 0,
"shown": 0,
"truncated": false
},
"evidence": {
"receipt": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.finalize.json",
"summaryReceipt": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.finalize-summary.json",
"browserCheckReceipt": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.browser-check.json"
},
"update": {
"status": "current",
"installedVersion": "3.0.1",
"availableVersion": "3.0.1",
"releaseNotes": null,
"checkedAt": "2026-09-29T09:50:05.829Z",
"source": "cache",
"noticeRequired": false,
"noticeText": null
},
"visualReview": "not-requested",
"durationMs": 10108,
"visualReviewRecommendation": {
"action": "inspect-route-readability",
"signals": {
"routesOverSuggestedBends": 1
},
"reason": "Automated gates passed, but crossings or detours still need perceptual review before claiming visual quality.",
"affectedRoutes": {
"crossings": [],
"detours": [
{
"relationship": {
"id": "token-only",
"from": "flows",
"to": "tokens",
"label": "token only",
"collectionIndex": 9,
"artifactIndex": 10
},
"bends": 3,
"stretch": 1.087,
"directCorridorBlockers": [
{
"id": "sessions",
"label": "Trading session setup",
"box": [
360,
560,
190,
74
]
}
]
}
],
"truncated": false
},
"repair": "Trace these relationships at the desktop viewport. For Architecture, use references/architecture-layout-repair.md: reflow a blocked main path or tangled connected scene, and repair an isolated defect locally only when the surrounding composition is accepted. Preserve all semantic content and user-fixed geometry. Rerun finalize once after the edit."
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,430 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "PXIL Session + Trading Token Setup",
"subtitle": "Configuration-driven pytest/Selenium workflow",
"output": ".archify/workflow-pxil-session-token-20260929-160100/pxil-session-token.html",
"quality_profile": "showcase",
"animation": "none",
"repository": {
"url": "https://git.tejasshasane.xyz/tejasshasane/pxil-selenium-automation.git",
"revision": "5ab89ce0059525b31558a1fa82251a2a6f610dd7",
"link_mode": "local-only"
}
},
"lanes": [
{
"id": "config",
"label": "Configuration \u0026 selection"
},
{
"id": "pytest",
"label": "Pytest orchestration"
},
{
"id": "browser",
"label": "Browser automation"
},
{
"id": "portal",
"label": "Pratyay portal"
}
],
"phases": [
{
"id": "resolve",
"label": "Resolve run",
"fromCol": 0,
"toCol": 1
},
{
"id": "login",
"label": "Authenticate",
"fromCol": 2,
"toCol": 2,
"variant": "security"
},
{
"id": "setup",
"label": "Create session + tokens",
"fromCol": 3,
"toCol": 4,
"variant": "emphasis"
},
{
"id": "evidence",
"label": "Capture evidence",
"fromCol": 5,
"toCol": 5,
"variant": "dashed"
}
],
"groups": [
{
"id": "config_group",
"label": "Product and mode resolution",
"lane": "config",
"fromCol": 0,
"toCol": 1,
"variant": "dashed"
},
{
"id": "auth_group",
"label": "One browser login",
"lane": "browser",
"fromCol": 2,
"toCol": 2,
"variant": "security"
},
{
"id": "session_group",
"label": "Session creation gate",
"lane": "portal",
"fromCol": 3,
"toCol": 3,
"variant": "emphasis"
},
{
"id": "token_group",
"label": "Token creation loop",
"lane": "portal",
"fromCol": 4,
"toCol": 5,
"variant": "emphasis"
},
{
"id": "evidence_group",
"label": "Step artifacts",
"lane": "config",
"fromCol": 5,
"toCol": 5,
"variant": "dashed"
}
],
"mainPath": [
"pytest_start",
"load_config",
"parametrize",
"driver_login",
"token_dispatch",
"token_save",
"save_report"
],
"nodes": [
{
"id": "pytest_start",
"lane": "pytest",
"col": 0,
"type": "backend",
"label": "Pytest setup test",
"sublabel": "test_combined_setup.py",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 11,
"end_line": 14
}
]
},
{
"id": "load_config",
"lane": "config",
"col": 1,
"type": "database",
"label": "Load config.json",
"sublabel": "products + setup flags",
"width": 150,
"sources": [
{
"path": "tests/config_reader.py",
"line": 6,
"end_line": 33
}
]
},
{
"id": "parametrize",
"lane": "config",
"col": 2,
"type": "backend",
"label": "get_setup_configs",
"sublabel": "combined / session / token",
"tag": "skip disabled",
"width": 150,
"sources": [
{
"path": "tests/config_reader.py",
"line": 36,
"end_line": 83
}
]
},
{
"id": "driver_login",
"lane": "browser",
"col": 2,
"type": "security",
"label": "Open + login",
"sublabel": "Chrome, credentials, CAPTCHA",
"tag": "manual CAPTCHA",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 21,
"end_line": 29
},
{
"path": "tests/helpers.py",
"line": 60,
"end_line": 91
}
]
},
{
"id": "session_gate",
"lane": "pytest",
"col": 3,
"type": "security",
"label": "run_session?",
"sublabel": "product flag gate",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 31,
"end_line": 32
}
]
},
{
"id": "session_create",
"lane": "portal",
"col": 3,
"type": "backend",
"label": "create_session",
"sublabel": "ME instance + dates -\u003e Save",
"width": 150,
"sources": [
{
"path": "tests/setup/sessions/test_session.py",
"line": 24,
"end_line": 40
}
]
},
{
"id": "token_loop",
"lane": "pytest",
"col": 4,
"type": "backend",
"label": "Product-code loop",
"sublabel": "one token per configured code",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 34,
"end_line": 37
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 74,
"end_line": 83
}
]
},
{
"id": "token_dispatch",
"lane": "portal",
"col": 4,
"type": "backend",
"label": "run_token_setup",
"sublabel": "IDAS / RTM / UPC branch",
"width": 150,
"sources": [
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 254,
"end_line": 262
}
]
},
{
"id": "token_save",
"lane": "portal",
"col": 5,
"type": "backend",
"label": "Create + save token",
"sublabel": "dates, expiry, time sessions",
"width": 150,
"sources": [
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 264,
"end_line": 325
}
]
},
{
"id": "save_report",
"lane": "config",
"col": 5,
"type": "database",
"label": "Evidence + teardown",
"sublabel": "screenshots, backup, logout",
"width": 150,
"sources": [
{
"path": "tests/step_actions.py",
"line": 21,
"end_line": 40
},
{
"path": "tests/helpers.py",
"line": 135,
"end_line": 159
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 330,
"end_line": 342
}
]
},
{
"id": "session_skip",
"lane": "config",
"col": 3,
"type": "messagebus",
"label": "Skip session",
"sublabel": "run_session = false",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 31,
"end_line": 32
}
]
},
{
"id": "pratyay_ui",
"lane": "portal",
"col": 2,
"type": "external",
"label": "Pratyay UI",
"sublabel": "Trading maintenance pages",
"width": 150,
"sources": [
{
"path": "tests/setup/sessions/test_session.py",
"line": 28,
"end_line": 40
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 274,
"end_line": 325
}
]
}
],
"edges": [
{
"id": "start-config",
"from": "pytest_start",
"to": "load_config",
"label": "collect + resolve",
"variant": "emphasis"
},
{
"id": "config-param",
"from": "load_config",
"to": "parametrize",
"label": "product settings"
},
{
"id": "param-login",
"from": "parametrize",
"to": "driver_login",
"label": "selected config",
"variant": "emphasis"
},
{
"id": "login-portal",
"from": "driver_login",
"to": "pratyay_ui",
"label": "authenticate",
"variant": "security"
},
{
"id": "skip-token-loop",
"from": "session_skip",
"to": "token_loop",
"label": "token-only / combined decision",
"variant": "dashed"
},
{
"id": "dispatch-save",
"from": "token_dispatch",
"to": "token_save",
"label": "IDAS default",
"variant": "emphasis"
},
{
"id": "save-evidence",
"from": "token_save",
"to": "save_report",
"label": "step captures",
"variant": "dashed"
},
{
"id": "login-dispatch",
"from": "driver_login",
"to": "token_dispatch",
"label": "run token flow",
"variant": "emphasis"
}
],
"cards": [
{
"dot": "cyan",
"title": "How configuration is called",
"items": [
"pytest parametrization calls get_setup_configs(\"combined\") when test_session_and_token_setup is collected.",
"get_product_config merges product overrides into setup.run_session/run_token and resolves token settings; disabled products are filtered out.",
"Each selected config carries url, login, product.me_instance, token settings and product_name into the test."
]
},
{
"dot": "amber",
"title": "Combined run semantics",
"items": [
"The test opens the portal and logs in once per browser fixture. It saves a browser cookie/session backup after login.",
"When run_session is true, create_session selects the configured ME instance and saves the calculated start/end window.",
"The same driver then loops product codes and calls run_token_setup for each one."
]
},
{
"dot": "violet",
"title": "Token branches",
"items": [
"IDAS/default creates the token, fills delivery/expiry values, then edits ORDER_NOR, ORDER_EXCH and TRADE time sessions before Save and Activate.",
"RTM selects market/product/generation mode, fills delivery and session times, then saves; activation is not part of this branch.",
"UPC fills configured order/trade times, saves, then resets the generator."
]
},
{
"dot": "rose",
"title": "Authentication and evidence",
"items": [
"login() sends credentials, waits for manual CAPTCHA completion, submits, waits for the dashboard marker and closes transient notices.",
"StepActions captures screenshots after interactions. conftest.py attaches them to pytest-html and creates report folders.",
"The diagram shows trading sessions/tokens; login session backups are browser cookies, not the trading-session records created in the portal."
]
}
]
}
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"receiptId": "194f6ca1-621e-45b4-ab5d-d915aa4711fd",
"status": "current",
"command": "deliver",
"type": "workflow",
"input": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\candidate.json",
"output": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.html",
"specification": {
"sha256": "c349c44844d853c1dc013cd9f1e9380b0f1548224ef389a1960b97af956310b1",
"bytes": 19583
},
"artifact": {
"sha256": "d0c3abadeefa664a3d46893e7dc43042339bf42471f76967fe467600f0388882",
"bytes": 771409
}
}
@@ -0,0 +1,89 @@
{
"schemaVersion": 1,
"ok": true,
"command": "finalize",
"status": "pass",
"type": "workflow",
"quality": "showcase",
"specification": {
"path": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\candidate.json",
"sha256": "c349c44844d853c1dc013cd9f1e9380b0f1548224ef389a1960b97af956310b1",
"bytes": 19583
},
"artifact": {
"path": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.html",
"sha256": "d0c3abadeefa664a3d46893e7dc43042339bf42471f76967fe467600f0388882",
"bytes": 771409
},
"gates": {
"validate": "pass",
"deliver": "pass",
"check": "pass",
"browser-check": "pass"
},
"diagnostics": [],
"diagnosticSummary": {
"total": 0,
"shown": 0,
"truncated": false
},
"evidence": {
"receipt": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.finalize.json",
"summaryReceipt": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.finalize-summary.json",
"browserCheckReceipt": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.browser-check.json"
},
"update": {
"status": "current",
"installedVersion": "3.0.1",
"availableVersion": "3.0.1",
"releaseNotes": null,
"checkedAt": "2026-09-29T09:50:05.829Z",
"source": "cache",
"noticeRequired": false,
"noticeText": null
},
"visualReview": "not-requested",
"durationMs": 12243,
"visualReviewRecommendation": {
"action": "inspect-route-readability",
"signals": {
"routesOverSuggestedBends": 2
},
"reason": "Automated gates passed, but crossings or detours still need perceptual review before claiming visual quality.",
"affectedRoutes": {
"crossings": [],
"detours": [
{
"relationship": {
"id": "login-dispatch",
"from": "driver_login",
"to": "token_dispatch",
"label": "run token flow",
"collectionIndex": 2,
"artifactIndex": 3
},
"bends": 3,
"stretch": 1.164
},
{
"relationship": {
"id": "param-login",
"from": "parametrize",
"to": "driver_login",
"label": "selected config",
"collectionIndex": 4,
"artifactIndex": 5
},
"bends": 3,
"stretch": 1.109
}
],
"truncated": false
},
"hints": [
"driver_login → token_dispatch needs 3 bends: move node driver_login or token_dispatch so they share a row or column with matching centers, or sit diagonally with a clear corner.",
"parametrize → driver_login needs 3 bends: move node parametrize or driver_login so they share a row or column with matching centers, or sit diagonally with a clear corner."
],
"repair": "Trace these relationships at the desktop viewport. For Architecture, use references/architecture-layout-repair.md: reflow a blocked main path or tangled connected scene, and repair an isolated defect locally only when the surrounding composition is accepted. Preserve all semantic content and user-fixed geometry. Rerun finalize once after the edit."
}
}
File diff suppressed because one or more lines are too long
+6
View File
@@ -9,6 +9,9 @@ __pycache__/
# Pytest cache # Pytest cache
.pytest_cache/ .pytest_cache/
# Machine-specific test configuration
config.json
# Generated reports (keep directory structure) # Generated reports (keep directory structure)
reports/screenshots/* reports/screenshots/*
!reports/screenshots/.gitkeep !reports/screenshots/.gitkeep
@@ -18,3 +21,6 @@ reports/logs/*
reports/html/* reports/html/*
!reports/html/.gitkeep !reports/html/.gitkeep
# CAPTCHA stress-test screenshots and CSV reports
reports/captcha_login_stress/
+249
View File
@@ -0,0 +1,249 @@
{
"url": "https://utilseastern.pxil.in/Pxil/",
"stagingurl": "https://utilseastern.pxil.in/Pxil/",
"uaturl": "https://utilsuat.pxil.in/Pxil/",
"login": {
"username": "Tejas",
"password": "Pxil@123"
},
"login_captcha_wait_seconds": 10,
"login_captcha_max_retries": 10,
"login_post_submit_wait_seconds": 10,
"step_delay_seconds": 1,
"setup": {
"run_session": true,
"run_token": false,
"session": {
"time_mode": "offset",
"start_offset_minutes": 5,
"start_time": "14:30",
"end_time": "20:30"
}
},
"products": {
"IDAS": {
"enabled": true,
"run_session": true,
"run_token": false,
"me_instance": "55",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "IDAS",
"product_code": "IDAS4",
"product_codes": "IDAS,IDASTEST01,IDASTEST02,IDASTEST03,IDASTEST04,IDASTEST05,IDASTEST06",
"available_codes": ",IDASTEST06,IDASTEST07,IDASTEST08,IDASTEST09,IDASTEST10,IDASTEST11,IDASTEST12,IDASTEST13,IDASTEST14,IDASTEST15,IDASTEST16,IDASTEST17,IDASTEST18,IDASTEST19,IDASTEST20,IDASTEST21,IDASTEST22,IDASTEST23,IDASTEST24,IDASTEST25,IDASTEST26,IDASTEST27,IDASTEST28,IDASTEST29,IDASTEST30",
"delivery_offset_days": 1,
"expiry_time": "20:30",
"time_sessions": {
"ORDER_NOR": {
"start_offset_minutes": 10,
"end_time": "19:30"
},
"ORDER_EXCH": {
"start_time": "19:30",
"end_time": "19:45"
},
"TRADE": {
"start_time": "19:45",
"end_time": "19:50"
}
}
}
},
"RTM": {
"enabled": false,
"run_session": false,
"run_token": true,
"me_instance": "52",
"session": {
"time_mode": "offset",
"start_offset_minutes": 7,
"start_time": "14:30",
"end_time": "20:30"
},
"token": {
"market_code": "NATIONAL",
"product_type": "RTM",
"product_codes": [
"RTM"
],
"generation_mode": "RTM",
"delivery_offset_days": 0,
"delivery_from_time": "00:00",
"delivery_to_time": "23:59",
"time_mode": "offset",
"session_start_offset_minutes": 5,
"session_start_time": "14:30",
"session_end_time": "20:00"
}
},
"UPC": {
"enabled": true,
"run_session": true,
"run_token": true,
"session_variants": {
"DAILY": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "58",
"token": {
"generation_mode": "AnyDayUPC",
"create_product_codes": [
"DailyC_E"
],
"available_product_codes": [
"DailyC_D",
"DailyGR_D",
"DailyC_RTC",
"DailyC_E",
"DailyC_M"
]
}
},
"WEEKLY": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "59",
"token": {
"generation_mode": "WeeklyUPC",
"create_product_codes": [
"WeeklyC_E"
],
"available_product_codes": [
"WeeklyC_D",
"WeeklyC_N",
"WeeklyGR_D",
"WeeklyC_RTC"
]
}
},
"MONTHLY": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "60",
"token": {
"generation_mode": "MonthlyUPC",
"create_product_codes": [
"MonthlyC_N"
],
"available_product_codes": [
"MonthlyC_D",
"MonthlyGR_D",
"MonthlyC_RTC"
]
}
}
},
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "UPC",
"product_codes": [],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"upc_times": {
"time_mode": "manual",
"order_normal_start_time": "13:00",
"order_normal_start_offset_minutes": 10,
"order_normal_end_time": "17:00",
"trade_session_start_time": "17:00",
"trade_session_end_time": "22:00"
}
}
},
"RA": {
"enabled": false,
"run_session": true,
"run_token": false,
"me_instance": "64",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "RA",
"product_codes": [],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"time_sessions": {}
}
},
"REC": {
"enabled": false,
"run_session": true,
"run_token": true,
"me_instance": "25",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "REC",
"product_codes": [
"REC"
],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"type_prefix": "REC",
"time_sessions": {
"ORDER_NOR": {
"time_mode": "offset",
"start_offset_minutes": 10,
"start_time": "14:30",
"end_time": "19:30"
},
"ORDER_EXCH": {
"start_time": "19:30",
"end_time": "19:45"
},
"TRADE": {
"start_time": "19:45",
"end_time": "19:50"
}
}
}
},
"INTRADAY": {
"enabled": true,
"run_session": true,
"run_token": true,
"me_instance": "53",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "INTRADAY",
"product_codes": [
"I_DayStatic_C"
],
"generation_mode": "Intraday24by7",
"delivery_start_offset_minutes": 5,
"trading_start_time": "00:20",
"trading_start_days_before_delivery": 1,
"duration_before_delivery_end_time": 150,
"delivery_to_time": "23:59"
}
},
"DAC": {
"enabled": false,
"run_session": true,
"run_token": true,
"me_instance": "57",
"session": {
"time_mode": "offset",
"start_offset_minutes": 5,
"end_time": "23:00"
},
"token": {
"market_code": "NATIONAL",
"product_type": "DAC",
"product_codes": ["DACStatic"],
"generation_mode": "DAC",
"delivery_start_offset_minutes": 5,
"delivery_to_time": "23:59",
"session_start_time": "13:00",
"session_end_time": "23:00",
"delivery_time_period": 15
}
}
}
}
-177
View File
@@ -1,177 +0,0 @@
{
"url": "https://utilseastern.pxil.in/Pxil/",
"stagingurl": "https://utilseastern.pxil.in/Pxil/",
"uaturl": "https://utilsuat.pxil.in/Pxil/",
"login": {
"username": "Tejas",
"password": "Pxil@123"
},
"login_captcha_wait_seconds": 10,
"login_post_submit_wait_seconds": 10,
"step_delay_seconds": 1,
"setup": {
"run_session": true,
"run_token": true,
"session": {
"start_offset_minutes": 7,
"end_time": "20:30"
}
},
"products": {
"IDAS": {
"enabled": false,
"me_instance": "55",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "IDAS",
"product_code": "IDAS4",
"product_codes": "IDAS,IDASTEST01,IDASTEST02,IDASTEST03,IDASTEST04,IDASTEST05,IDASTEST06",
"available_codes": ",IDASTEST06,IDASTEST07,IDASTEST08,IDASTEST09,IDASTEST10,IDASTEST11,IDASTEST12,IDASTEST13,IDASTEST14,IDASTEST15,IDASTEST16,IDASTEST17,IDASTEST18,IDASTEST19,IDASTEST20,IDASTEST21,IDASTEST22,IDASTEST23,IDASTEST24,IDASTEST25,IDASTEST26,IDASTEST27,IDASTEST28,IDASTEST29,IDASTEST30",
"delivery_offset_days": 1,
"expiry_time": "20:30",
"time_sessions": {
"ORDER_NOR": {
"start_offset_minutes": 7,
"end_time": "19:30"
},
"ORDER_EXCH": {
"start_time": "19:30",
"end_time": "19:45"
},
"TRADE": {
"start_time": "19:45",
"end_time": "19:50"
}
}
}
},
"RTM": {
"enabled": false,
"run_session": false,
"run_token": true,
"me_instance": "52",
"session": {
"start_offset_minutes": 7,
"end_time": "20:30"
},
"token": {
"market_code": "NATIONAL",
"product_type": "RTM",
"product_codes": [
"RTM"
],
"generation_mode": "RTM",
"delivery_offset_days": 0,
"delivery_from_time": "00:00",
"delivery_to_time": "23:59",
"session_start_offset_minutes": 5,
"session_end_time": "20:00"
}
},
"UPC": {
"enabled": false,
"run_session": true,
"run_token": true,
"session_variants": {
"DAILY": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "58",
"token": {
"generation_mode": "AnyDayUPC",
"create_product_codes": [
"DailyC_E"
],
"available_product_codes": [
"DailyC_D",
"DailyGR_D",
"DailyC_RTC",
"DailyC_E",
"DailyC_M"
]
}
},
"WEEKLY": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "59",
"token": {
"generation_mode": "WeeklyUPC",
"create_product_codes": [
"WeeklyC_E"
],
"available_product_codes": [
"WeeklyC_D",
"WeeklyC_N",
"WeeklyGR_D",
"WeeklyC_RTC"
]
}
},
"MONTHLY": {
"enabled": true,
"run_session": true,
"run_token": true,
"me_instance": "60",
"token": {
"generation_mode": "MonthlyUPC",
"create_product_codes": [
"MonthlyC_N"
],
"available_product_codes": [
"MonthlyC_D",
"MonthlyGR_D",
"MonthlyC_RTC"
]
}
}
},
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "UPC",
"product_codes": [],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"upc_times": {
"order_normal_start_time": "13:00",
"order_normal_end_time": "17:00",
"trade_session_start_time": "17:00",
"trade_session_end_time": "22:00"
},
"time_sessions": {}
}
},
"RA": {
"enabled": true,
"run_session": true,
"run_token": false,
"me_instance": "64",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "RA",
"product_codes": [],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"time_sessions": {}
}
},
"REC": {
"enabled": false,
"me_instance": "",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "REC",
"product_codes": [],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"time_sessions": {}
}
}
}
}
+6
View File
@@ -12,3 +12,9 @@ markers =
reports: report download coverage reports: report download coverage
order_book: order book coverage order_book: order book coverage
trade_book: trade book coverage trade_book: trade book coverage
captcha: CAPTCHA login stress test
filterwarnings =
ignore::DeprecationWarning:easyocr.*
ignore::DeprecationWarning:torch.*
ignore::UserWarning:torch.*
+10 -2
View File
@@ -1,6 +1,14 @@
# Core test runner # Core test runner
pytest==8.4.1 pytest==9.1.1
pytest-html==4.2.0 pytest-html==4.2.0
# Browser automation # Browser automation
selenium==4.34.2 selenium==4.50.0
# CAPTCHA OCR and image processing
easyocr==1.7.2
opencv-python==5.0.0.93
Pillow==12.3.0
numpy==2.5.3
torch==2.14.1
torchvision==0.29.1
+10 -1
View File
@@ -1,4 +1,7 @@
import os
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.chrome.service import Service
def create_chrome_driver(): def create_chrome_driver():
@@ -10,5 +13,11 @@ def create_chrome_driver():
options.add_argument("--disable-features=CalculateNativeWinOcclusion") options.add_argument("--disable-features=CalculateNativeWinOcclusion")
options.add_argument("--disable-hang-monitor") options.add_argument("--disable-hang-monitor")
options.add_argument("--disable-popup-blocking") options.add_argument("--disable-popup-blocking")
options.add_argument("--disable-background-networking")
options.add_argument("--disable-component-update")
options.add_argument("--disable-sync")
options.add_argument("--disable-default-apps")
options.add_experimental_option("excludeSwitches", ["enable-logging"])
return webdriver.Chrome(options=options) service = Service(log_output=os.devnull)
return webdriver.Chrome(service=service, options=options)
+353
View File
@@ -0,0 +1,353 @@
"""
CAPTCHA Solver for PXIL Login using EasyOCR.
Extracts base64 from input's CSS background-image, runs EasyOCR OCR, fills the input.
"""
import re
import base64
import time
from typing import Optional
from PIL import Image
import cv2
import numpy as np
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
# Initialize EasyOCR once (module level)
# First run downloads models, then works offline
try:
import easyocr
_EASYOCR_READER = easyocr.Reader(['en'], gpu=False)
except ImportError:
_EASYOCR_READER = None
# Character confusion correction map for common OCR errors
CONFUSION_MAP = {
'0': 'O', 'O': '0', 'D': '0',
'1': 'I', 'I': '1', 'L': '1', 'l': '1',
'5': 'S', 'S': '5',
'8': 'B', 'B': '8',
'2': 'Z', 'Z': '2',
'6': 'G', 'G': '6',
'9': 'g', 'g': '9',
'Q': 'O',
'U': 'V', 'V': 'U',
'n': 'h', 'h': 'n',
'r': 'f', 'f': 'r',
'c': 'e', 'e': 'c',
'o': '0', '0': 'o',
's': '5', '5': 's',
'z': '2', '2': 'z',
'i': '1', '1': 'i',
'l': '1', '1': 'l',
}
def find_visible_captcha_input(driver: WebDriver) -> WebElement:
"""Find the visible captcha input element (name='captcha')."""
wait = WebDriverWait(driver, 15)
inputs = wait.until(EC.presence_of_all_elements_located((By.NAME, "captcha")))
for inp in inputs:
if inp.is_displayed():
return inp
raise RuntimeError("No visible captcha input found")
def extract_captcha_base64(captcha_input: WebElement) -> bytes:
"""Extract base64 image data from input's style attribute. Returns raw JPEG bytes."""
style = captcha_input.get_attribute("style") or ""
match = re.search(r'background:\s*url\(["\']?data:image/[^;]+;base64,([^"\')]+)["\']?\)', style)
if not match:
raise RuntimeError(f"No base64 background-image found in style: {style[:200]}")
b64_data = match.group(1)
if not b64_data:
raise RuntimeError("Base64 data is empty (captcha not loaded yet)")
b64_data += "=" * ((4 - len(b64_data) % 4) % 4) # Fix padding
return base64.b64decode(b64_data)
def wait_for_captcha_load(driver: WebDriver, captcha_input: WebElement, timeout: int = 15) -> bool:
"""Wait for captcha base64 to be populated with actual image data."""
end_time = time.time() + timeout
while time.time() < end_time:
try:
style = captcha_input.get_attribute("style") or ""
match = re.search(r'background:\s*url\(["\']?data:image/[^;]+;base64,([^"\')]+)["\']?\)', style)
if match and match.group(1):
return True
except StaleElementReferenceException:
captcha_input = find_visible_captcha_input(driver)
time.sleep(0.3)
return False
def preprocess_captcha_image(image_bytes: bytes) -> Image.Image:
"""Preprocess image for better OCR accuracy - gentle preprocessing."""
nparr = np.frombuffer(image_bytes, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # Load as color (BGR)
if img is None:
raise RuntimeError("Failed to decode image bytes")
# Convert BGR to RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# Simple threshold - keep it simple
_, gray = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)
# Light denoise
gray = cv2.fastNlMeansDenoising(gray, h=3)
# Very light morphological close - only connect broken parts
kernel = np.ones((1, 1), np.uint8)
gray = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
# Resize 6x for better recognition (larger for 6-char captcha)
gray = cv2.resize(gray, None, fx=6, fy=6, interpolation=cv2.INTER_CUBIC)
# Convert back to RGB (3-channel) for EasyOCR
rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
return Image.fromarray(rgb)
def apply_confusion_corrections(text: str) -> str:
"""Apply character confusion corrections based on common OCR errors."""
corrected = []
for ch in text:
corrected.append(CONFUSION_MAP.get(ch, ch))
return ''.join(corrected)
def ocr_captcha(image: Image.Image) -> str:
"""Run OCR on preprocessed captcha image using EasyOCR."""
if _EASYOCR_READER is None:
raise RuntimeError("EasyOCR not installed. Run: pip install easyocr")
img_array = np.array(image)
# EasyOCR with alphanumeric allowlist for 6-char captcha
results = _EASYOCR_READER.readtext(
img_array,
detail=0,
allowlist='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
width_ths=0.5,
paragraph=False
)
# Combine all detected text
result = ''.join(results)
# Clean result - keep only alphanumeric, max 6 chars
cleaned = ''.join(filter(str.isalnum, result))[:6]
print(f" DEBUG OCR: result='{result}', cleaned='{cleaned}', len={len(cleaned)}")
return cleaned
def find_refresh_button(driver: WebDriver, captcha_input: WebElement) -> Optional[WebElement]:
"""Find captcha refresh button near the captcha input."""
parent = captcha_input.find_element(By.XPATH, "..")
for sibling in parent.find_elements(By.XPATH, "*"):
onclick = (sibling.get_attribute("onclick") or "").lower()
class_attr = (sibling.get_attribute("class") or "").lower()
id_attr = (sibling.get_attribute("id") or "").lower()
if any(kw in onclick for kw in ["refresh", "captcha", "reload", "newcaptcha"]):
return sibling
if any(kw in class_attr for kw in ["refresh", "captcha-refresh", "reload"]):
return sibling
if any(kw in id_attr for kw in ["refresh", "captcha", "reload"]):
return sibling
# Fallback: search globally for refresh-like elements
try:
candidates = driver.find_elements(By.XPATH, "//*[self::a or self::button or self::img or self::span][contains(translate(@onclick, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh') or contains(translate(@class, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh') or contains(translate(@title, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh')]")
for cand in candidates:
if cand.is_displayed():
return cand
except:
pass
return None
def wait_for_new_captcha(driver: WebDriver, captcha_input: WebElement, timeout: int = 10) -> bool:
"""Wait for captcha background-image to change (new challenge loaded)."""
old_style = captcha_input.get_attribute("style") or ""
end_time = time.time() + timeout
while time.time() < end_time:
try:
new_style = captcha_input.get_attribute("style") or ""
if new_style != old_style and "data:image" in new_style:
return True
except StaleElementReferenceException:
captcha_input = find_visible_captcha_input(driver)
old_style = captcha_input.get_attribute("style") or ""
time.sleep(0.3)
return False
def solve_and_fill_captcha(driver: WebDriver, max_retries: int = 5, attempt_callback=None) -> bool:
"""
Main entry point: solve captcha and fill input.
Returns True if filled successfully, False if max retries exceeded.
Each retry gets a fresh captcha via refresh button.
"""
for attempt in range(1, max_retries + 1):
captcha_text = ""
try:
captcha_input = find_visible_captcha_input(driver)
# Wait for captcha base64 to be populated (async load)
if not wait_for_captcha_load(driver, captcha_input):
print(f" [Attempt {attempt}] Captcha failed to load in time, retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
if attempt_callback:
attempt_callback(attempt, "", "", "not_loaded")
continue
image_bytes = extract_captcha_base64(captcha_input)
pil_image = preprocess_captcha_image(image_bytes)
captcha_text = ocr_captcha(pil_image)
if not captcha_text or len(captcha_text) < 6:
print(f" [Attempt {attempt}] OCR weak result: '{captcha_text}' (len={len(captcha_text)}), retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
if attempt_callback:
attempt_callback(attempt, captcha_text, "", "ocr_failed")
continue
# Apply confusion corrections to get best variant
corrected = apply_confusion_corrections(captcha_text)
# Generate case variants for case-sensitive captcha (mixed case + numbers)
variants = [
captcha_text, # as-is (from OCR)
corrected, # with confusion corrections
captcha_text.upper(), # all uppercase
captcha_text.capitalize(), # first letter uppercase
captcha_text.title(), # each word capitalized
captcha_text.swapcase(), # swap case
]
# Add variations with first char variations
if len(captcha_text) >= 1:
variants.append(captcha_text[0].upper() + captcha_text[1:])
variants.append(captcha_text[0].lower() + captcha_text[1:])
if len(captcha_text) >= 2:
variants.append(captcha_text[:2].upper() + captcha_text[2:])
variants.append(captcha_text[:2].lower() + captcha_text[2:])
if len(captcha_text) >= 3:
variants.append(captcha_text[:3].upper() + captcha_text[3:])
variants.append(captcha_text[:3].lower() + captcha_text[3:])
# Remove duplicates while preserving order, keep only 6-char variants
seen = set()
unique_variants = []
for v in variants:
if v not in seen and len(v) == 6:
seen.add(v)
unique_variants.append(v)
variants = unique_variants
if attempt_callback:
screenshot_path = attempt_callback(attempt, captcha_text, "", "before_entry") or ""
success = False
# Try ALL variants before giving up on this attempt
for variant in variants:
if len(variant) == 6:
print(f" [Attempt {attempt}] Trying variant: '{variant}'")
captcha_input.clear()
captcha_input.send_keys(variant)
success = True
if attempt_callback:
attempt_callback(attempt, captcha_text, variant, "filled")
break
if not success:
print(f" [Attempt {attempt}] No valid 6-char variant found for '{captcha_text}', retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
# Click submit
submit_btn = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//button[@value='Login']"))
)
submit_btn.click()
# Wait a bit for response
time.sleep(2)
# Check for invalid captcha error using the exact xpath
try:
# First handle any alerts that might be open
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected after submit: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
invalid_captcha_elem = WebDriverWait(driver, 3).until(
EC.visibility_of_element_located((
By.XPATH,
"//div[@id='error']//strong[contains(normalize-space(), 'Invalid Captcha')]",
))
)
print(f" [Attempt {attempt}] Invalid captcha detected: {invalid_captcha_elem.text}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
except TimeoutException:
# No invalid captcha error - likely success
print(f" [Attempt {attempt}] No invalid captcha error detected, assuming success")
return True
except Exception as e:
# Handle other exceptions like NoSuchElementException
print(f" [Attempt {attempt}] Error checking invalid captcha: {type(e).__name__}: {e}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
except Exception as e:
print(f" [Attempt {attempt}] Error: {e}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
return False
+8 -2
View File
@@ -46,7 +46,10 @@ def get_setup_configs(mode, config=None):
variants = product.get("session_variants") variants = product.get("session_variants")
if variants is not None: if variants is not None:
resolved = get_product_config(name, config) resolved = get_product_config(name, config)
if not resolved["setup"].get("run_session", True): # Expand variants when either session or token execution is
# enabled. Token-only UPC runs must still reach their variants.
setup = resolved.get("setup", {})
if not setup.get("run_session", True) and not setup.get("run_token", True):
continue continue
variant_config = deepcopy(config) variant_config = deepcopy(config)
variant_config["products"] = {} variant_config["products"] = {}
@@ -64,7 +67,10 @@ def get_setup_configs(mode, config=None):
session = setup.get("run_session", True) session = setup.get("run_session", True)
token = setup.get("run_token", True) token = setup.get("run_token", True)
matches = { matches = {
"combined": session and token or (name.startswith("UPC_") and token), # The full setup run owns every enabled product that has work to do.
# Products without tokens (for example RA) still belong in this
# shared browser flow; the combined test skips only their token step.
"combined": session or token,
"session": session and not token, "session": session and not token,
"token": token and not session, "token": token and not session,
} }
+24
View File
@@ -47,6 +47,30 @@ def pytest_configure(config):
config.option.self_contained_html = True config.option.self_contained_html = True
def pytest_collection_modifyitems(config, items):
"""Keep the full setup run limited to the shared combined flow."""
invocation = " ".join(
str(arg).replace("\\", "/") for arg in config.invocation_params.args
).lower()
full_setup_run = (
"tests/setup" in invocation
and "tests/setup/sessions" not in invocation
and "tests/setup/tokens" not in invocation
and "test_session.py" not in invocation
and "test_trading_token.py" not in invocation
)
if not full_setup_run:
return
deselected = [
item for item in items
if "/tests/setup/sessions/" in f"/{item.nodeid.replace('\\', '/')}"
or "/tests/setup/tokens/" in f"/{item.nodeid.replace('\\', '/')}"
]
items[:] = [item for item in items if item not in deselected]
config.hook.pytest_deselected(items=deselected)
# ----------------------------- # -----------------------------
# WebDriver Fixture # WebDriver Fixture
# ----------------------------- # -----------------------------
+221 -37
View File
@@ -57,43 +57,216 @@ def enter_pressed():
return pressed return pressed
def login(driver, username, password): def check_captcha_error(driver) -> str:
"""Check if there's a CAPTCHA error message on the page."""
try:
# First handle any alerts
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected in check_captcha_error: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
error_selectors = [
"//div[@id='error']//strong[normalize-space()='Invalid Captcha']",
"//div[@id='error']//span[contains(@class,'help-block')]//strong[contains(text(),'Invalid')]",
"//*[contains(@class,'error') and contains(text(),'Invalid Captcha')]",
]
for selector in error_selectors:
elements = driver.find_elements(By.XPATH, selector)
for el in elements:
if el.is_displayed() and el.text.strip():
return el.text.strip()
except Exception as e:
print(f" DEBUG: check_captcha_error exception: {e}")
return ""
def check_login_success(driver) -> bool:
"""Check if login was successful using multiple indicators."""
try:
# Handle any pending alerts first
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
# Wait a bit for page to load after submit
time.sleep(3)
# Print current URL and page title for debugging
print(f" DEBUG: Current URL: {driver.current_url}")
print(f" DEBUG: Page title: {driver.title}")
# Multiple dashboard indicators
dashboard_indicators = [
"//span[normalize-space()='TRADING MAINTENANCE']",
"//a[contains(@href, 'tradingMaintenance')]",
"//span[contains(text(), 'Dashboard')]",
"//h1[contains(text(), 'Dashboard')]",
"//div[contains(@class, 'dashboard')]",
"//a[contains(text(), 'Trading')]",
"//div[contains(@class, 'navbar')]//span[contains(text(), 'Trading')]",
]
wait = WebDriverWait(driver, 15)
for indicator in dashboard_indicators:
try:
wait.until(EC.visibility_of_element_located((By.XPATH, indicator)))
print(f" DEBUG: Found dashboard indicator: {indicator}")
return True
except:
continue
# URL-based check
if "home" in driver.current_url.lower() or "dashboard" in driver.current_url.lower():
print(" DEBUG: Login success detected via URL")
return True
return False
except Exception as e:
print(f" DEBUG: check_login_success failed: {e}")
return False
def login(
driver,
username,
password,
max_captcha_retries=5,
captcha_metrics=None,
max_login_duration_seconds=1200,
):
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
# username # --- AUTOMATED CAPTCHA SOLVING WITH RETRY ON FAILURE ---
userid_field = wait.until( from tests.captcha_solver import solve_and_fill_captcha, find_refresh_button, wait_for_new_captcha
EC.visibility_of_element_located((By.XPATH, Locator.userid))
) login_deadline = time.monotonic() + max_login_duration_seconds
userid_field.send_keys(username) captcha_attempt = 0
while True:
captcha_attempt += 1
# password # Refreshing after a failed CAPTCHA clears the login form, so refill
password_field = driver.find_element(By.XPATH, Locator.password) # both credentials before every automatic or manual retry.
password_field.send_keys(password) userid_field = wait.until(
EC.visibility_of_element_located((By.XPATH, Locator.userid))
)
userid_field.clear()
userid_field.send_keys(username)
print(f"Solve captcha within {LOGIN_CAPTCHA_WAIT_SECONDS} seconds...") password_field = wait.until(
EC.visibility_of_element_located((By.XPATH, Locator.password))
)
password_field.clear()
password_field.send_keys(password)
# wait for manual captcha solve if captcha_metrics is not None:
time.sleep(LOGIN_CAPTCHA_WAIT_SECONDS) captcha_metrics["tries"] = captcha_attempt
captcha_metrics["max_retries"] = max_captcha_retries
print(f"Solving CAPTCHA (attempt {captcha_attempt})...")
# click submit # Once automatic solving has run for the configured duration, ask for
submit_btn = wait.until( # the CAPTCHA value in a browser prompt and continue the same login flow.
EC.element_to_be_clickable((By.XPATH, Locator.submitbtn)) if time.monotonic() >= login_deadline:
) print("Automatic CAPTCHA retries timed out; waiting for manual CAPTCHA entry.")
submit_btn.click() manual_value = driver.execute_script(
"return window.prompt('Automatic CAPTCHA solving timed out. Enter the CAPTCHA shown on the page:');"
)
if not manual_value:
raise RuntimeError("Manual CAPTCHA entry was cancelled")
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS) captcha_input = wait.until(
EC.visibility_of_element_located((By.NAME, "captcha"))
)
captcha_input.clear()
captcha_input.send_keys(manual_value.strip())
wait.until(EC.element_to_be_clickable((By.XPATH, Locator.submitbtn))).click()
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
accept_browser_alert_if_present(driver)
handle_transient_popups(driver)
accept_browser_alert_if_present(driver) captcha_error = check_captcha_error(driver)
if captcha_error:
print(f" Manual CAPTCHA rejected: {captcha_error}")
continue
# wait for dashboard if check_login_success(driver):
wait.until( print("Login successful after manual CAPTCHA entry")
EC.visibility_of_element_located((By.XPATH, Locator.dashboard_marker)) if captcha_metrics is not None:
) captcha_metrics["correct_attempt"] = captcha_attempt
captcha_metrics.setdefault("captcha_attempts", []).append({
close_tac_expiry_modal_if_present(driver) "attempt": captcha_attempt,
"captcha_text": "manual",
print("Login successful") "variant": manual_value.strip(),
"status": "manual",
"screenshot": "",
"error": "",
})
close_tac_expiry_modal_if_present(driver)
return True
continue
def attempt_callback(attempt_num, captcha_text, variant, status, screenshot_path="", error=""):
if captcha_metrics is not None:
captcha_metrics.setdefault("captcha_attempts", []).append({
"attempt": attempt_num,
"captcha_text": captcha_text,
"variant": variant,
"status": status,
"screenshot": screenshot_path,
"error": error
})
if not solve_and_fill_captcha(driver, max_retries=1, attempt_callback=attempt_callback):
print(" Automatic CAPTCHA solve failed; retrying until the login deadline.")
driver.refresh()
time.sleep(3)
continue
# solve_and_fill_captcha already clicks submit, so just wait and check result
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
accept_browser_alert_if_present(driver)
# Handle any popups that appear
handle_transient_popups(driver)
# Check for CAPTCHA error (invalid captcha message)
captcha_error = check_captcha_error(driver)
if captcha_error:
print(f" CAPTCHA validation failed: {captcha_error}")
# Reload the page to get a fresh CAPTCHA and keep retrying until
# the overall login deadline is reached.
print(f" Reloading page to get fresh CAPTCHA (attempt {captcha_attempt + 1})...")
driver.refresh()
time.sleep(3)
continue
# Check for successful login - use improved detection
if check_login_success(driver):
print("Login successful")
if captcha_metrics is not None:
captcha_metrics["correct_attempt"] = captcha_attempt
close_tac_expiry_modal_if_present(driver)
return True
# If we get here, something else is wrong
print(" Login not confirmed, retrying...")
driver.refresh()
time.sleep(3)
def accept_browser_alert_if_present(driver, timeout=5): def accept_browser_alert_if_present(driver, timeout=5):
@@ -309,23 +482,34 @@ def open_trading_maintenance_page(driver, menu_xpath):
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
trading_maintenance = wait.until( def visible_elements(xpath):
EC.presence_of_element_located((By.XPATH, Locator.tradingmaintenance)) return [element for element in driver.find_elements(By.XPATH, xpath) if element.is_displayed()]
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", trading_maintenance)
submenu = driver.find_elements(By.XPATH, menu_xpath) submenu_visible = any(
submenu_visible = any(item.is_displayed() for item in submenu) item.is_enabled() for item in visible_elements(menu_xpath)
)
if not submenu_visible: if not submenu_visible:
maintenance_items = visible_elements(Locator.tradingmaintenance)
if not maintenance_items:
wait.until(lambda current_driver: visible_elements(Locator.tradingmaintenance))
maintenance_items = visible_elements(Locator.tradingmaintenance)
maintenance = maintenance_items[0]
driver.execute_script(
"arguments[0].scrollIntoView({block: 'center'});", maintenance
)
try: try:
wait.until(EC.element_to_be_clickable((By.XPATH, Locator.tradingmaintenance))) wait.until(lambda current_driver: maintenance.is_enabled())
trading_maintenance.click() maintenance.click()
except Exception: except Exception:
driver.execute_script("arguments[0].click();", trading_maintenance) driver.execute_script("arguments[0].click();", maintenance)
menu_item = wait.until( menu_item = wait.until(
EC.element_to_be_clickable((By.XPATH, menu_xpath)) lambda current_driver: next(
(item for item in visible_elements(menu_xpath) if item.is_enabled()),
False,
)
) )
try: try:
menu_item.click() menu_item.click()
+15 -2
View File
@@ -4,11 +4,13 @@ class Locator(object):
userid="//input[@placeholder='User ID']" userid="//input[@placeholder='User ID']"
password="//input[@placeholder='Password']" password="//input[@placeholder='Password']"
submitbtn="//button[@value='Login']" submitbtn="//button[@value='Login']"
dashboard_marker="//span[normalize-space()='TRADING MAINTENANCE']" dashboard_marker="//*[contains(normalize-space(), 'TRADING MAINTENANCE')]"
tacexpiryclosebtn="//div[contains(@class,'modal') and contains(@style,'display: block')]//button[contains(@class,'close')]" tacexpiryclosebtn="//div[contains(@class,'modal') and contains(@style,'display: block')]//button[contains(@class,'close')]"
usermenutoggle="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//a[contains(@class,'dropdown-toggle') and @data-toggle='dropdown']" usermenutoggle="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//a[contains(@class,'dropdown-toggle') and @data-toggle='dropdown']"
logoutlink="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//ul[contains(@class,'dropdown-menu')]//a[@ng-click='logout()' and normalize-space()='Log Out']" logoutlink="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//ul[contains(@class,'dropdown-menu')]//a[@ng-click='logout()' and normalize-space()='Log Out']"
tradingmaintenance="//span[normalize-space()='TRADING MAINTENANCE']" # Click the Angular menu anchor, not the nested label span. The anchor
# owns openSubMenu(item), which is required to render its child links.
tradingmaintenance="//a[.//span[normalize-space()='TRADING MAINTENANCE']]"
tradingsession="//a[normalize-space()='Trading Session Master']" tradingsession="//a[normalize-space()='Trading Session Master']"
tokengeneration="//a[@href='#/tradingMaintenance/generateToken' or normalize-space()='Token Generation']" tokengeneration="//a[@href='#/tradingMaintenance/generateToken' or normalize-space()='Token Generation']"
tokengenerator = "//a[@href='#/tradingMaintenance/tokenGenerator']" tokengenerator = "//a[@href='#/tradingMaintenance/tokenGenerator']"
@@ -36,6 +38,17 @@ class Locator(object):
tokendeliverystartdate = "//input[@placeholder='DELIVERY_START_DATE']" tokendeliverystartdate = "//input[@placeholder='DELIVERY_START_DATE']"
tokendeliveryenddate = "//input[@placeholder='DELIVERY_END_DATE']" tokendeliveryenddate = "//input[@placeholder='DELIVERY_END_DATE']"
tokenexpirydate = "//input[@placeholder='Token Expiry Date']" tokenexpirydate = "//input[@placeholder='Token Expiry Date']"
tokentype = "//input[@name='TYPE' or @placeholder='TYPE']"
intradaydeliveryfromdate = "//input[@placeholder='Delivery From Date']"
intradaydeliverytodate = "//input[@placeholder='Delivery To Date']"
intradaytradingstarttime = "//input[@placeholder='Trading Start Time']"
intradaydaysbeforedelivery = "//input[@placeholder='Trading Start days before delivery']"
intradaydurationbeforeend = "//input[@placeholder='Duration before delivery end time']"
intradaysuccessmessage = "//*[contains(normalize-space(text()), 'Tokens generated successfully')]"
dactradingfromdate = "//input[@placeholder='Trading From Date']"
dactradingtodate = "//input[@placeholder='Trading To Date']"
dacsessionstarttime = "//input[@placeholder='Session Start Time']"
dacsessionendtime = "//input[@placeholder='Session End Time']"
tokentradedate = "//input[@placeholder='Trade Date']" tokentradedate = "//input[@placeholder='Trade Date']"
upcordernormalstart = "//input[@name='ses3StartTime']" upcordernormalstart = "//input[@name='ses3StartTime']"
upcordernormalend = "//input[@name='ses3EndTime']" upcordernormalend = "//input[@name='ses3EndTime']"
+18 -4
View File
@@ -13,13 +13,21 @@ from tests.locators import Locator
def get_session_window(config): def get_session_window(config):
session_config = config.get("setup", {}).get("session", {}) session_config = config.get("setup", {}).get("session", {})
start_offset_minutes = session_config.get("start_offset_minutes", 7) time_mode = session_config.get("time_mode", "offset").lower()
if time_mode == "manual":
start_time = session_config.get("start_time")
if not start_time:
raise ValueError("Session time_mode 'manual' requires start_time")
elif time_mode == "offset":
start_offset_minutes = session_config.get("start_offset_minutes", 7)
start_time = (datetime.now() + timedelta(minutes=start_offset_minutes)).strftime("%H:%M")
else:
raise ValueError("Session time_mode must be 'manual' or 'offset'")
end_time = session_config.get("end_time", "20:30") end_time = session_config.get("end_time", "20:30")
start_datetime = datetime.now() + timedelta(minutes=start_offset_minutes)
today = datetime.now().strftime("%d/%m/%Y") today = datetime.now().strftime("%d/%m/%Y")
return start_datetime.strftime("%d/%m/%Y %H:%M"), f"{today} {end_time}" return f"{today} {start_time}", f"{today} {end_time}"
def create_session(driver, step, config): def create_session(driver, step, config):
@@ -44,7 +52,13 @@ def create_session(driver, step, config):
@pytest.mark.session @pytest.mark.session
@pytest.mark.parametrize("config", get_setup_configs("session"), ids=lambda c: c["product_name"]) @pytest.mark.parametrize("config", get_setup_configs("session"), ids=lambda c: c["product_name"])
def test_session(driver, test_context, config): def test_session(driver, test_context, config, request):
# The full setup command runs session-only products (such as RA) through
# test_combined_setup.py so they share the same browser/login context.
invocation = " ".join(str(arg).replace("\\", "/") for arg in request.config.invocation_params.args)
if "tests/setup" in invocation and "tests/setup/sessions" not in invocation and "test_session.py" not in invocation:
pytest.skip("Handled by the shared full setup flow")
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
+5 -4
View File
@@ -31,8 +31,9 @@ def test_session_and_token_setup(driver, test_context, config):
if config.get("setup", {}).get("run_session", True): if config.get("setup", {}).get("run_session", True):
create_session(driver, step, config) create_session(driver, step, config)
for product_code in get_product_codes(token): if config.get("setup", {}).get("run_token", True):
product_token = dict(token) for product_code in get_product_codes(token):
product_token["product_code"] = product_code product_token = dict(token)
run_token_setup(driver, step, product_token) product_token["product_code"] = product_code
run_token_setup(driver, step, product_token)
+161 -16
View File
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
import pytest import pytest
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from tests.config_reader import get_setup_configs, parse_product_codes from tests.config_reader import get_setup_configs, parse_product_codes
from tests.helpers import ( from tests.helpers import (
@@ -55,15 +56,28 @@ def get_delivery_date_and_expiry(token):
return delivery_date, token_expiry return delivery_date, token_expiry
def get_intraday_delivery_window(token, now=None):
"""Return an Intraday delivery window that cannot start in the past."""
now = datetime.now() if now is None else now
start = now + timedelta(minutes=token.get("delivery_start_offset_minutes", 5))
start_text = start.strftime("%d/%m/%Y %H:%M")
end_text = f"{start.strftime('%d/%m/%Y')} {token.get('delivery_to_time', '23:59')}"
return start_text, end_text
def get_time_session_times(token, row_text, default_start_time, default_end_time): def get_time_session_times(token, row_text, default_start_time, default_end_time):
time_session_config = token.get("time_sessions", {}).get(row_text, {}) time_session_config = token.get("time_sessions", {}).get(row_text, {})
time_mode = time_session_config.get("time_mode")
start_time = time_session_config.get("start_time") start_time = time_session_config.get("start_time")
if start_time is None: if time_mode == "offset":
start_offset_minutes = time_session_config.get("start_offset_minutes") start_offset_minutes = time_session_config.get("start_offset_minutes")
if start_offset_minutes is not None: if start_offset_minutes is None:
start_time = (datetime.now() + timedelta(minutes=start_offset_minutes)).strftime("%H:%M") raise ValueError(f"{row_text} offset mode requires start_offset_minutes")
start_time = (datetime.now() + timedelta(minutes=start_offset_minutes)).strftime("%H:%M")
elif time_mode == "manual" and not start_time:
raise ValueError(f"{row_text} manual mode requires start_time")
return ( return (
start_time or default_start_time, start_time or default_start_time,
@@ -71,6 +85,29 @@ def get_time_session_times(token, row_text, default_start_time, default_end_time
) )
def get_upc_order_normal_start_time(timings, now=None):
"""Resolve UPC order-normal start time from manual or offset mode."""
mode = timings.get("time_mode", "manual").lower()
if mode == "offset":
offset = timings.get("order_normal_start_offset_minutes")
if offset is None:
raise ValueError(
"UPC time_mode 'offset' requires "
"order_normal_start_offset_minutes"
)
now = datetime.now() if now is None else now
return (now + timedelta(minutes=offset)).strftime("%H:%M")
if mode != "manual":
raise ValueError("UPC time_mode must be 'manual' or 'offset'")
start_time = timings.get("order_normal_start_time")
if not start_time:
raise ValueError(
"UPC time_mode 'manual' requires order_normal_start_time"
)
return start_time
def get_product_codes(token): def get_product_codes(token):
product_codes = parse_product_codes(token.get("create_product_codes", token.get("product_codes"))) product_codes = parse_product_codes(token.get("create_product_codes", token.get("product_codes")))
@@ -133,11 +170,11 @@ def complete_time_session_edits(driver, step, token, next_day):
edit_time_session_row( edit_time_session_row(
driver, driver,
step, step,
"ORDER_NOR", "ORDER_NORMAL",
order_nor_start_time, order_nor_start_time,
order_nor_end_time order_nor_end_time
) )
edit_time_session_row(driver, step, "ORDER_EXCH", order_exch_start_time, order_exch_end_time) edit_time_session_row(driver, step, "ORDER_EXCHANGE", order_exch_start_time, order_exch_end_time)
edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time) edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time)
click_first_grid_button(driver, "Token Details") click_first_grid_button(driver, "Token Details")
@@ -150,6 +187,7 @@ def complete_time_session_edits(driver, step, token, next_day):
def complete_time_session_edits_from_saved_token(driver, step, token): def complete_time_session_edits_from_saved_token(driver, step, token):
step.click((By.XPATH, Locator.tokentimesessionbtn), "open_time_session") step.click((By.XPATH, Locator.tokentimesessionbtn), "open_time_session")
WebDriverWait(driver, 20).until(EC.url_contains("timeSessionView"))
click_first_grid_button(driver, "Time Session Details") click_first_grid_button(driver, "Time Session Details")
step.capture("open_time_session_details") step.capture("open_time_session_details")
step.capture("before_edit_order_normal") step.capture("before_edit_order_normal")
@@ -166,11 +204,11 @@ def complete_time_session_edits_from_saved_token(driver, step, token):
edit_time_session_row( edit_time_session_row(
driver, driver,
step, step,
"ORDER_NOR", "ORDER_NORMAL",
order_nor_start_time, order_nor_start_time,
order_nor_end_time order_nor_end_time
) )
edit_time_session_row(driver, step, "ORDER_EXCH", order_exch_start_time, order_exch_end_time) edit_time_session_row(driver, step, "ORDER_EXCHANGE", order_exch_start_time, order_exch_end_time)
edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time) edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time)
click_first_grid_button(driver, "Token Details") click_first_grid_button(driver, "Token Details")
@@ -201,10 +239,19 @@ def open_rtm_new_token(driver, step, token):
def get_rtm_token_times(token, now=None): def get_rtm_token_times(token, now=None):
now = datetime.now() if now is None else now now = datetime.now() if now is None else now
delivery_date = (now + timedelta(days=token.get("delivery_offset_days", 0))).strftime("%d/%m/%Y") delivery_date = (now + timedelta(days=token.get("delivery_offset_days", 0))).strftime("%d/%m/%Y")
time_mode = token.get("time_mode", "offset").lower()
if time_mode == "manual":
session_start_time = token.get("session_start_time")
if not session_start_time:
raise ValueError("RTM time_mode 'manual' requires session_start_time")
elif time_mode == "offset":
session_start_time = (now + timedelta(minutes=token.get("session_start_offset_minutes", 5))).strftime("%H:%M")
else:
raise ValueError("RTM time_mode must be 'manual' or 'offset'")
return ( return (
f"{delivery_date} {token.get('delivery_from_time', '00:00')}", f"{delivery_date} {token.get('delivery_from_time', '00:00')}",
f"{delivery_date} {token.get('delivery_to_time', '23:59')}", f"{delivery_date} {token.get('delivery_to_time', '23:59')}",
(now + timedelta(minutes=token.get("session_start_offset_minutes", 5))).strftime("%H:%M"), session_start_time,
token.get("session_end_time", "20:00"), token.get("session_end_time", "20:00"),
) )
@@ -225,6 +272,76 @@ def create_rtm_token(driver, step, token):
handle_transient_popups(driver) handle_transient_popups(driver)
def create_intraday_token(driver, step, token):
"""Create an Intraday token through the Token Generator page."""
open_trading_maintenance_page(driver, Locator.tokengenerator)
handle_transient_popups(driver)
step.capture("open_token_generator_intraday")
for element_id, value, name in (
(Locator.generatormarketcode, token["market_code"], "market_code"),
(Locator.generatorproductcode, token["product_code"], "product_code"),
(Locator.generatorgenerationmode, token["generation_mode"], "generation_mode"),
):
select_bootstrap_dropdown_by_id(driver, element_id, value, exact=True)
step.capture(f"select_{name}_intraday")
step.click((By.XPATH, Locator.addnewtokenbtn), "click_add_new_token_intraday")
delivery_start, delivery_end = get_intraday_delivery_window(token)
values = (
(Locator.intradaydeliveryfromdate, delivery_start, "delivery_from_date"),
(Locator.intradaydeliverytodate, delivery_end, "delivery_to_date"),
(Locator.intradaytradingstarttime, token.get("trading_start_time", "00:20"), "trading_start_time"),
(Locator.intradaydaysbeforedelivery, str(token.get("trading_start_days_before_delivery", 1)), "days_before_delivery"),
)
for locator, value, name in values:
type_first_visible_input(driver, locator, value)
step.capture(f"enter_{name}_intraday")
step.click((By.XPATH, Locator.savetokenbtn), "save_token_intraday")
WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.XPATH, Locator.intradaysuccessmessage))
)
step.capture("intraday_token_generated_successfully")
handle_transient_popups(driver)
def create_dac_token(driver, step, token):
"""Create a DAC token through the Token Generator page."""
open_trading_maintenance_page(driver, Locator.tokengenerator)
handle_transient_popups(driver)
step.capture("open_token_generator_dac")
for element_id, value, name in (
(Locator.generatormarketcode, token["market_code"], "market_code"),
(Locator.generatorproductcode, token["product_code"], "product_code"),
(Locator.generatorgenerationmode, token["generation_mode"], "generation_mode"),
):
select_bootstrap_dropdown_by_id(driver, element_id, value, exact=True)
step.capture(f"select_{name}_dac")
step.click((By.XPATH, Locator.addnewtokenbtn), "click_add_new_token_dac")
trading_start, trading_end = get_intraday_delivery_window(token)
values = (
(Locator.dactradingfromdate, trading_start, "trading_from_date"),
(Locator.dactradingtodate, trading_end, "trading_to_date"),
(Locator.dacsessionstarttime, token.get("session_start_time", "13:00"), "session_start_time"),
(Locator.dacsessionendtime, token.get("session_end_time", "23:00"), "session_end_time"),
)
for locator, value, name in values:
type_first_visible_input(driver, locator, value)
step.capture(f"enter_{name}_dac")
step.click((By.XPATH, Locator.savetokenbtn), "save_token_dac")
WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.XPATH, Locator.intradaysuccessmessage))
)
step.capture("dac_token_generated_successfully")
handle_transient_popups(driver)
def create_upc_token(driver, step, token): def create_upc_token(driver, step, token):
open_trading_maintenance_page(driver, Locator.tokengenerator) open_trading_maintenance_page(driver, Locator.tokengenerator)
handle_transient_popups(driver) handle_transient_popups(driver)
@@ -238,7 +355,7 @@ def create_upc_token(driver, step, token):
step.click((By.XPATH, Locator.addnewtokenbtn), "click_add_new_token_upc") step.click((By.XPATH, Locator.addnewtokenbtn), "click_add_new_token_upc")
timings = token.get("upc_times", {}) timings = token.get("upc_times", {})
for name, locator, value in ( for name, locator, value in (
("order_normal_start", Locator.upcorderstart, timings.get("order_normal_start_time")), ("order_normal_start", Locator.upcorderstart, get_upc_order_normal_start_time(timings)),
("order_normal_end", Locator.upcorderend, timings.get("order_normal_end_time")), ("order_normal_end", Locator.upcorderend, timings.get("order_normal_end_time")),
("trade_start", Locator.upctradestart, timings.get("trade_session_start_time")), ("trade_start", Locator.upctradestart, timings.get("trade_session_start_time")),
("trade_end", Locator.upctradeend, timings.get("trade_session_end_time")), ("trade_end", Locator.upctradeend, timings.get("trade_session_end_time")),
@@ -252,6 +369,12 @@ def create_upc_token(driver, step, token):
def run_token_setup(driver, step, token): def run_token_setup(driver, step, token):
if token.get("product_type", "").upper() == "DAC":
create_dac_token(driver, step, token)
return
if token.get("product_type", "").upper() == "INTRADAY":
create_intraday_token(driver, step, token)
return
if token.get("product_type", "").upper() == "UPC": if token.get("product_type", "").upper() == "UPC":
create_upc_token(driver, step, token) create_upc_token(driver, step, token)
return return
@@ -289,16 +412,23 @@ def create_and_activate_token(driver, step, token):
step.click((By.XPATH, Locator.addnewtokenbtn), f"click_add_new_token_{product_code.lower()}") step.click((By.XPATH, Locator.addnewtokenbtn), f"click_add_new_token_{product_code.lower()}")
next_day, token_expiry = get_delivery_date_and_expiry(token) is_intraday = token.get("product_type", "").upper() == "INTRADAY"
if is_intraday:
delivery_start, delivery_end = get_intraday_delivery_window(token)
else:
next_day, token_expiry = get_delivery_date_and_expiry(token)
if token.get("product_type", "").upper() == "REC":
token_type_prefix = token.get("type_prefix", "REC")
token_type = f"{token_type_prefix}-{datetime.now().strftime('%d%m%Y')}"
step.type((By.XPATH, Locator.tokentype), token_type, f"enter_type_{product_code.lower()}")
if token.get("product_type", "").upper() == "UPC": if token.get("product_type", "").upper() == "UPC":
today = datetime.now().strftime("%d/%m/%Y") today = datetime.now().strftime("%d/%m/%Y")
if token.get("generation_mode") == "AnyDayUPC": if token.get("generation_mode") == "AnyDayUPC":
step.type((By.XPATH, Locator.tokentradedate), today, f"enter_trade_date_{product_code.lower()}") step.type((By.XPATH, Locator.tokentradedate), today, f"enter_trade_date_{product_code.lower()}")
timings = token.get("upc_times", {}) timings = token.get("upc_times", {})
normal_start = timings.get("order_normal_start_time") normal_start = get_upc_order_normal_start_time(timings)
if normal_start is None and timings.get("order_normal_start_offset_minutes") is not None:
normal_start = (datetime.now() + timedelta(minutes=timings["order_normal_start_offset_minutes"])).strftime("%H:%M")
values = { values = {
Locator.upcordernormalstart: normal_start, Locator.upcordernormalstart: normal_start,
Locator.upcordernormalend: timings.get("order_normal_end_time"), Locator.upcordernormalend: timings.get("order_normal_end_time"),
@@ -310,9 +440,17 @@ def create_and_activate_token(driver, step, token):
continue continue
type_first_visible_input(driver, locator, value) type_first_visible_input(driver, locator, value)
if token.get("generation_mode") not in ("WeeklyUPC", "MonthlyUPC"): if is_intraday:
step.type((By.XPATH, Locator.tokendeliverystartdate), delivery_start, f"enter_delivery_start_date_{product_code.lower()}")
step.type((By.XPATH, Locator.tokendeliveryenddate), delivery_end, f"enter_delivery_end_date_{product_code.lower()}")
elif (
token.get("generation_mode") not in ("WeeklyUPC", "MonthlyUPC")
and token.get("product_type", "").upper() != "REC"
):
step.type((By.XPATH, Locator.tokendeliverystartdate), next_day, f"enter_delivery_start_date_{product_code.lower()}") step.type((By.XPATH, Locator.tokendeliverystartdate), next_day, f"enter_delivery_start_date_{product_code.lower()}")
step.type((By.XPATH, Locator.tokendeliveryenddate), next_day, f"enter_delivery_end_date_{product_code.lower()}") step.type((By.XPATH, Locator.tokendeliveryenddate), next_day, f"enter_delivery_end_date_{product_code.lower()}")
if not is_intraday and token.get("generation_mode") not in ("WeeklyUPC", "MonthlyUPC"):
step.type((By.XPATH, Locator.tokenexpirydate), token_expiry, f"enter_token_expiry_date_{product_code.lower()}") step.type((By.XPATH, Locator.tokenexpirydate), token_expiry, f"enter_token_expiry_date_{product_code.lower()}")
step.click((By.XPATH, Locator.savetokenbtn), f"save_token_{product_code.lower()}") step.click((By.XPATH, Locator.savetokenbtn), f"save_token_{product_code.lower()}")
@@ -322,12 +460,19 @@ def create_and_activate_token(driver, step, token):
step.click((By.XPATH, Locator.tokengeneratorresetbtn), f"reset_token_generator_{product_code.lower()}") step.click((By.XPATH, Locator.tokengeneratorresetbtn), f"reset_token_generator_{product_code.lower()}")
return return
complete_time_session_edits_from_saved_token(driver, step, token) if not is_intraday:
complete_time_session_edits_from_saved_token(driver, step, token)
@pytest.mark.trading_token @pytest.mark.trading_token
@pytest.mark.parametrize("config", get_setup_configs("token"), ids=lambda c: c["product_name"]) @pytest.mark.parametrize("config", get_setup_configs("token"), ids=lambda c: c["product_name"])
def test_trading_token_creation(driver, test_context, config): def test_trading_token_creation(driver, test_context, config, request):
# The full setup command runs token-enabled products through the combined
# flow so session and token creation use one browser/login context.
invocation = " ".join(str(arg).replace("\\", "/") for arg in request.config.invocation_params.args)
if "tests/setup" in invocation and "tests/setup/tokens" not in invocation and "test_trading_token.py" not in invocation:
pytest.skip("Handled by the shared full setup flow")
config, step, token = get_token_context(driver, test_context, config) config, step, token = get_token_context(driver, test_context, config)
open_app_and_login(driver, step, config) open_app_and_login(driver, step, config)
+124
View File
@@ -0,0 +1,124 @@
import csv
import os
import time
from datetime import datetime
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from tests.browser import create_chrome_driver
from tests.config_reader import load_config
from tests.helpers import login, logout
from tests.locators import Locator
LOGIN_ATTEMPTS = 50
MAX_CAPTCHA_RETRIES = 7
@pytest.mark.captcha
def test_captcha_login_stress():
"""Run bounded EasyOCR login attempts and retain auditable CAPTCHA evidence."""
config = load_config()
output_dir = os.path.abspath(os.path.join("reports", "captcha_login_stress"))
os.makedirs(output_dir, exist_ok=True)
started = datetime.now().strftime("%Y%m%d_%H%M%S")
csv_path = os.path.join(output_dir, f"captcha_login_{started}.csv")
rows = []
passed = 0
failed = 0
def record(row):
rows.append(row)
for login_number in range(1, LOGIN_ATTEMPTS + 1):
login_started = datetime.now().isoformat(timespec="seconds")
captcha_rows = []
driver = create_chrome_driver()
try:
driver.get(config["url"])
login_status = "FAILED"
logout_status = "NOT_ATTEMPTED"
error_text = ""
captcha_metrics = {"captcha_attempts": []}
try:
login(
driver,
config["login"]["username"],
config["login"]["password"],
max_captcha_retries=MAX_CAPTCHA_RETRIES,
captcha_metrics=captcha_metrics,
max_login_duration_seconds=1200,
)
login_status = "PASSED"
passed += 1
except Exception as exc:
failed += 1
error_text = f"{type(exc).__name__}: {exc}"
try:
driver.save_screenshot(os.path.join(output_dir, f"login_{login_number:03d}_failure.png"))
except Exception:
pass
finally:
# Explicitly log out after a successful authenticated flow,
# then close the browser session.
if login_status == "PASSED":
try:
logout_status = "PASSED" if logout(driver) else "FAILED"
except Exception as exc:
logout_status = "FAILED"
print(f"Logout error: {type(exc).__name__}: {exc}")
try:
driver.quit()
except Exception:
pass
captcha_rows = captcha_metrics.get("captcha_attempts", []) or [{
"attempt": 0, "captcha_text": "", "variant": "",
"status": "no_captcha_record", "screenshot": "", "error": error_text,
}]
for item in captcha_rows:
record({
"login_attempt": login_number,
"started_at": login_started,
"login_status": login_status,
"logout_status": logout_status,
"captcha_attempt": item["attempt"],
"attempted_captcha": item["variant"],
"ocr_captcha": item["captcha_text"],
"screenshot_path": item["screenshot"],
"captcha_status": item["status"],
"captcha_entered_count": sum(
1 for captcha_row in captcha_rows if captcha_row["variant"]
),
"error": item["error"] or error_text,
"attempts_to_result": len(captcha_rows),
"passed_count": passed,
"failed_count": failed,
})
finally:
# Clean up the driver after all attempts
try:
driver.quit()
except Exception:
pass
fieldnames = [
"login_attempt", "started_at", "login_status", "captcha_attempt",
"logout_status",
"attempted_captcha", "ocr_captcha", "screenshot_path", "captcha_status",
"captcha_entered_count", "error", "attempts_to_result", "passed_count",
"failed_count",
]
with open(csv_path, "w", newline="", encoding="utf-8") as report:
writer = csv.DictWriter(report, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"CAPTCHA stress report: {csv_path}")
print(f"Passed: {passed}; Failed: {failed}; Total: {LOGIN_ATTEMPTS}")
+35
View File
@@ -78,3 +78,38 @@ def test_upc_session_selection_and_shared_timing(selected):
assert config == original assert config == original
config["products"]["UPC"]["enabled"] = False config["products"]["UPC"]["enabled"] = False
assert get_setup_configs("session", config) == [] assert get_setup_configs("session", config) == []
def test_upc_token_only_expands_all_enabled_variants():
config = {
"setup": {"run_session": True, "run_token": True},
"products": {
"UPC": {
"enabled": True,
"run_session": False,
"run_token": True,
"token": {
"market_code": "NATIONAL",
"product_type": "UPC",
"product_codes": [],
},
"session_variants": {
name: {
"enabled": True,
"run_session": False,
"run_token": True,
"token": {"create_product_codes": [f"{name}_TOKEN"]},
}
for name in ("DAILY", "WEEKLY", "MONTHLY")
},
}
},
}
configs = get_setup_configs("token", config)
assert [item["product_name"] for item in configs] == [
"UPC_DAILY",
"UPC_WEEKLY",
"UPC_MONTHLY",
]