@arcadialdev/arcality 2.4.28 → 2.4.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +4 -7
- package/scripts/gen-and-run.mjs +5 -18
- package/src/KnowledgeService.ts +256 -0
- package/src/consoleBanner.ts +32 -0
- package/src/envSetup.ts +205 -0
- package/src/index.ts +25 -0
- package/src/projectInspector.ts +42 -0
- package/src/services/collectiveMemoryService.ts +178 -0
- package/src/services/securityScanner.ts +128 -0
- package/src/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +733 -0
- package/tests/_helpers/agentic-runner.bundle.spec.js +2912 -83
- package/tests/_helpers/agentic-runner.spec.ts +783 -0
- package/tests/_helpers/ai-agent-helper.ts +1660 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-security-tools.ts +265 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
|
@@ -1,56 +1,1403 @@
|
|
|
1
|
-
var $e=Object.create;var we=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var Ne=Object.getOwnPropertyNames;var Ue=Object.getPrototypeOf,Pe=Object.prototype.hasOwnProperty;var De=(o,e,i,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of Ne(e))!Pe.call(o,l)&&l!==i&&we(o,l,{get:()=>e[l],enumerable:!(a=Oe(e,l))||a.enumerable});return o};var Z=(o,e,i)=>(i=o!=null?$e(Ue(o)):{},De(e||!o||!o.__esModule?we(i,"default",{value:o,enumerable:!0}):i,o));var Se=require("@playwright/test");var Me={name:"validate_element_state",description:"QA Skill: Validate that an element meets expected conditions (exists, visible, enabled, disabled, contains text, has value). Use this for assertions during test execution WITHOUT executing actions. This is the KEY tool for verifying application state.",input_schema:{type:"object",properties:{idx:{type:"number",description:"The IDX of the element to validate"},validations:{type:"array",description:"List of validations to perform on this element",items:{type:"object",properties:{type:{enum:["exists","visible","enabled","disabled","contains_text","has_value","is_empty","has_class"],description:"Type of validation to perform"},expected_value:{type:"string",description:"Expected value (required for: contains_text, has_value, has_class)"}},required:["type"]}}},required:["idx","validations"]}},je={name:"extract_table_data",description:"QA Skill: Extract structured data from tables or lists on the page. Use this to validate datasets, counts, or specific values. Returns an array of objects representing rows with their data.",input_schema:{type:"object",properties:{table_identifier:{type:"string",description:"Text content near the table, aria-label, or distinctive text that identifies the table (e.g., 'Timesheet - Hoy', 'Listado de pendientes')"},max_rows:{type:"number",description:"Maximum number of rows to extract (default: 50)",default:50},filter_column:{type:"string",description:"Optional: Column name to filter by"},filter_value:{type:"string",description:"Optional: Value to match in the filter column"}},required:["table_identifier"]}},Fe={name:"capture_console_errors",description:"QA Skill: Capture JavaScript console errors, warnings, and network failures that occurred during the test. Use this to detect client-side bugs that are not visible in the UI. This is CRITICAL for detecting hidden issues.",input_schema:{type:"object",properties:{severity_filter:{enum:["all","error","warning"],description:"Filter by severity level (default: error)",default:"error"},include_network_errors:{type:"boolean",description:"Include failed network requests (HTTP errors)",default:!0}}}},Ye={name:"assert_url_pattern",description:"QA Skill: Validate that the current URL matches (or doesn't match) an expected pattern. Use this to ensure navigation worked correctly or to verify you're on the expected page.",input_schema:{type:"object",properties:{pattern:{type:"string",description:"URL pattern to match (can be a substring like '/welcome' or a full URL)"},should_match:{type:"boolean",description:"True if URL should contain the pattern, false if it should NOT",default:!0}},required:["pattern"]}},qe={name:"measure_page_performance",description:"QA Skill: Measure page load time, network requests, and performance metrics (FCP, DOM loaded, total load). Use this to validate performance requirements or SLAs. Returns measured values and PASS/FAIL against optional thresholds.",input_schema:{type:"object",properties:{metrics_to_capture:{type:"array",items:{enum:["load_time","dom_content_loaded","first_contentful_paint","network_requests","resource_size"]},description:"Metrics to measure (default: all available)"},thresholds:{type:"object",description:"Optional thresholds for PASS/FAIL evaluation (e.g., {load_time_ms: 3000, dom_content_loaded_ms: 2000})",properties:{load_time_ms:{type:"number",description:"Maximum acceptable page load time in ms"},dom_content_loaded_ms:{type:"number",description:"Maximum acceptable DOM content loaded time in ms"},first_contentful_paint_ms:{type:"number",description:"Maximum acceptable FCP in ms"},max_network_requests:{type:"number",description:"Maximum number of network requests allowed"},max_resource_size_kb:{type:"number",description:"Maximum total resource size in KB"}}}}}},Be={name:"save_navigation_checkpoint",description:"QA Skill: Save the current page state (URL, cookies, localStorage, sessionStorage) as a named checkpoint. Use this before testing multiple paths from the same starting point, so you can restore later without repeating setup steps.",input_schema:{type:"object",properties:{checkpoint_name:{type:"string",description:"Unique name for this checkpoint (e.g., 'form_filled', 'logged_in', 'before_submit')"},include_storage:{type:"boolean",description:"Save localStorage and sessionStorage (default: true)",default:!0}},required:["checkpoint_name"]}},He={name:"restore_navigation_checkpoint",description:"QA Skill: Restore a previously saved checkpoint to return to that page state. This navigates back to the saved URL and restores cookies/storage. Use this to test alternative paths from the same starting point without re-running the entire flow.",input_schema:{type:"object",properties:{checkpoint_name:{type:"string",description:"Name of the checkpoint to restore (must have been saved previously)"}},required:["checkpoint_name"]}},Ge={name:"create_test_evidence",description:"QA Skill: Create visual test evidence (screenshots or annotated screenshots) and save them to disk. Use 'screenshot' for a plain capture, 'annotated_screenshot' to highlight specific elements with colored borders and labels, or 'full_page_screenshot' for a full-page capture. Evidence files are saved for bug reports and test documentation.",input_schema:{type:"object",properties:{evidence_type:{enum:["screenshot","annotated_screenshot","full_page_screenshot"],description:"Type of evidence to create"},annotations:{type:"array",description:"Elements to highlight with labels (only for 'annotated_screenshot')",items:{type:"object",properties:{idx:{type:"number",description:"IDX of the element to highlight"},label:{type:"string",description:"Annotation text to display near the element"},color:{type:"string",description:"Highlight color (default: red)",default:"red"}},required:["idx","label"]}},save_as:{type:"string",description:"Filename for the evidence (without extension, .png will be added). Example: 'bug_save_button_disabled'"},description:{type:"string",description:"Description of what this evidence proves or highlights"},finish_run:{type:"boolean",description:"If true, immediately ends the mission after saving evidence (saves tokens). Use this for final success evidence."}},required:["evidence_type","save_as"]}},Ve={name:"send_keyboard_event",description:"QA Skill: Send a keyboard event to the page or a specific element. Use this for: pressing Escape to close modals/popups, pressing Tab to navigate between form fields, pressing Enter to submit or confirm, pressing Arrow keys to navigate within date/time pickers, dropdowns, sliders, or any custom component that responds to keyboard input. This is CRITICAL for native browser controls (time pickers, date pickers, color pickers) that do not respond well to 'fill'.",input_schema:{type:"object",properties:{key:{type:"string",description:"The key to press. Supported values: 'Escape', 'Enter', 'Tab', 'Backspace', 'Delete', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Space', or any single character like 'a', '1', etc. For key combinations use '+' separator: 'Control+a', 'Shift+Tab'."},idx:{type:"number",description:"Optional: IDX of the element to focus before sending the key. If not provided, the key is sent to whatever element currently has focus."},repeat:{type:"number",description:"Number of times to press the key (default: 1). Useful for ArrowUp/ArrowDown in time pickers.",default:1}},required:["key"]}},Ke={name:"interact_native_control",description:"QA Skill: Interact with native HTML controls that standard click/fill may not handle correctly. This includes: <input type='time'>, <input type='date'>, <input type='datetime-local'>, <input type='color'>, <input type='range'>, <select> with optgroup, and contenteditable elements. This tool uses Playwright's specialized methods (selectOption, fill with format coercion, press sequences) to reliably set values. ALWAYS prefer this tool over manual fill for time/date inputs.",input_schema:{type:"object",properties:{idx:{type:"number",description:"The IDX of the native control element"},control_type:{enum:["time","date","datetime","color","range","select","contenteditable"],description:"The type of native control"},value:{type:"string",description:"The value to set. Formats: time='HH:mm' or 'HH:mm:ss', date='YYYY-MM-DD', datetime='YYYY-MM-DDTHH:mm', color='#rrggbb', range='number', select='option text or value', contenteditable='text to type'."}},required:["idx","control_type","value"]}},ze={name:"scroll_page",description:"QA Skill: Scroll the page or a specific container to reveal elements that are outside the current viewport. Use this when: you cannot find an expected element in CURRENT COMPONENTS (it might be below the fold), a form has many fields and you need to see more, or you need to scroll to a specific element. After scrolling, the page state will be refreshed in the next turn.",input_schema:{type:"object",properties:{direction:{enum:["up","down","left","right","top","bottom"],description:"Direction to scroll. 'top' and 'bottom' scroll all the way to the start/end."},amount:{type:"number",description:"Pixels to scroll (default: 500). Ignored for 'top' and 'bottom' directions.",default:500},idx:{type:"number",description:"Optional: IDX of a scrollable container. If omitted, scrolls the main page."}},required:["direction"]}},be=[Me,je,Fe,Ye,qe,Be,He,Ge,Ve,Ke,ze];async function ue(o){console.log(`[QA-SEC] Auditing HTTP Headers for: ${o.url()}`);let i=(await o.reload({waitUntil:"domcontentloaded"}))?.headers(),a=[],l={"Content-Security-Policy":{severity:"Medium",remediation:"Implement a strict Content-Security-Policy (CSP) to mitigate XSS and other injection attacks. Start with a restrictive policy and gradually allow trusted sources.",impact:"If an injection issue exists elsewhere in the application, the lack of CSP may increase exploitability and impact.",cwe:"CWE-16"},"Strict-Transport-Security":{severity:"Medium",remediation:"Implement HTTP Strict Transport Security (HSTS) to force browsers to communicate over HTTPS, preventing downgrade attacks.",impact:"Man-in-the-middle attacks could downgrade the connection to HTTP, allowing traffic interception.",cwe:"CWE-319"},"X-Content-Type-Options":{severity:"Low",remediation:"Set `X-Content-Type-Options: nosniff` to prevent browsers from MIME-sniffing a response away from the declared content-type.",impact:"Browsers may interpret non-executable MIME types as executable, leading to potential XSS.",cwe:"CWE-430"},"X-Frame-Options":{severity:"Medium",remediation:"Set `X-Frame-Options: SAMEORIGIN` or `DENY` to protect against clickjacking attacks by preventing the page from being loaded in an iframe on other domains.",impact:"Attackers could trick users into clicking visually hidden elements, causing unintended actions (Clickjacking).",cwe:"CWE-1021"}};for(let[h,w]of Object.entries(l))(!i||!Object.keys(i).find(R=>R.toLowerCase()===h.toLowerCase()))&&a.push({type:"security_misconfiguration",category:"Missing Security Header",subcategory:h,severity:w.severity,confidence:"High",exploitability:"not_confirmed",status:"detected",description:`The '${h}' HTTP header is missing. This header is crucial for defending against various web attacks.`,impact:w.impact,evidence:{url:o.url(),missingHeader:h},reproductionSteps:[`Navigate to ${o.url()}`,"Inspect the response headers.",`Verify that ${h} is absent.`],remediation:w.remediation,classification:{owaspTop10:"A05:2021 Security Misconfiguration",cwe:w.cwe}});return console.log(`[QA-SEC] Found ${a.length} missing security headers.`),a}async function pe(o){console.log("[QA-SEC] Scanning localStorage and sessionStorage for sensitive data.");let e=[],i=["token","password","secret","key","jwt","auth"],a={localStorage:await o.evaluate(()=>Object.entries(window.localStorage)),sessionStorage:await o.evaluate(()=>Object.entries(window.sessionStorage))};for(let[l,h]of Object.entries(a))for(let[w,R]of h)i.some(b=>w.toLowerCase().includes(b))&&e.push({type:"potential_issue",category:"Sensitive Data Exposure",subcategory:"Browser Storage",severity:"Medium",confidence:"Medium",exploitability:"unknown",status:"detected",description:`Potentially sensitive data was found in ${l}. Storing sensitive information like tokens or keys in browser storage is insecure.`,impact:"If an attacker discovers an XSS vulnerability, they can read localStorage/sessionStorage and exfiltrate secrets.",evidence:{storage:l,key:w,valuePreview:R?`${R.substring(0,10)}...`:"(empty)"},reproductionSteps:[`Open Developer Tools on ${o.url()}`,`Check ${l} for the key '${w}'.`],remediation:"Avoid storing sensitive data in browser storage. Use secure, HttpOnly cookies for session tokens. If you must store data, encrypt it first.",classification:{owaspTop10:"A04:2021 Insecure Design",cwe:"CWE-312"}});return console.log(`[QA-SEC] Found ${e.length} instances of sensitive data in browser storage.`),e}var K=Z(require("fs")),ie=Z(require("path"));var Ae=Z(require("crypto")),V=Z(require("chalk")),me=class o{static instance;apiBase;apiKey;projectId=null;constructor(){this.apiBase=process.env.ARCALITY_API_URL||"https://arcalityqadev.arcadial.lat",this.apiKey=process.env.ARCALITY_API_KEY||"",this.projectId=process.env.ARCALITY_PROJECT_ID||null}async fetchWithTimeout(e,i={},a=1e4){let l=new AbortController,h=setTimeout(()=>l.abort(),a);try{let w=await fetch(e,{...i,signal:l.signal});return clearTimeout(h),w}catch(w){throw clearTimeout(h),w}}static getInstance(){return o.instance||(o.instance=new o),o.instance}setProjectId(e){this.projectId=e}getProjectId(){return(!this.projectId||this.projectId==="undefined"||this.projectId==="null")&&(this.projectId=process.env.ARCALITY_PROJECT_ID||null),this.projectId}async getProjects(){try{let e=await this.fetchWithTimeout(`${this.apiBase}/api/v1/projects`,{headers:{"x-api-key":this.apiKey}});return e.ok?(await e.json()).projects||[]:[]}catch(e){return console.warn(V.default.yellow(`[KnowledgeService] Error al obtener proyectos: ${e.message}`)),[]}}async createProject(e,i){try{let a=await this.fetchWithTimeout(`${this.apiBase}/api/v1/projects`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify({name:e,base_url:i})});return a.ok?await a.json():null}catch(a){return console.error(V.default.red(`[KnowledgeService] Fall\xF3 creaci\xF3n de proyecto: ${a.message}`)),null}}async ingest(e){if(!e.project_id||e.project_id==="undefined"){console.warn(V.default.yellow("[Knowledge] Ingesta cancelada: project_id es inv\xE1lido."));return}try{let i=await this.fetchWithTimeout(`${this.apiBase}/api/v1/portal/ingest`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify(e)});i.status===200&&((await i.json()).status==="skipped_duplicate_dom"||console.log(V.default.cyan(`[Knowledge] Ingesta exitosa (Project: ${e.project_id})`)))}catch(i){console.warn(V.default.yellow(`[Knowledge] Error en ingesta: ${i.message}`))}}async getContext(e){let i=this.getProjectId();if(!i||i==="undefined")return null;try{let a=new URL(e).pathname,l=await this.fetchWithTimeout(`${this.apiBase}/api/v1/portal/context?project_id=${i}&path=${encodeURIComponent(a)}`,{headers:{"x-api-key":this.apiKey}});return l.ok?await l.json():null}catch(a){return console.warn(V.default.yellow(`[Knowledge] Error al obtener contexto: ${a.message}`)),null}}async saveRule(e,i,a="important",l){let h=this.getProjectId();if(!(!h||h==="undefined"))try{let w=l?new URL(l).pathname:null;await fetch(`${this.apiBase}/api/v1/portal/rules`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify({project_id:h,rule_type:"UI_UX",title:e,description:i,severity:a})}),console.log(V.default.green(`[Knowledge] Nueva regla registrada: ${e}`))}catch{}}async saveField(e,i,a){let l=this.getProjectId();if(!(!l||l==="undefined"))try{await fetch(`${this.apiBase}/api/v1/portal/fields`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify({project_id:l,field_identifier:e,field_type:i,is_required:a,source:"agent_auto"})}),console.log(V.default.green(`[Knowledge] Nuevo campo catalogado: ${e}`))}catch{}}async saveKnowledge(e,i,a="PROCESS"){let l=this.getProjectId();if(!(!l||l==="undefined"))try{await fetch(`${this.apiBase}/api/v1/portal/knowledge`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify({project_id:l,doc_type:a,title:e,content:i,source:"agent_auto"})}),console.log(V.default.green(`[Knowledge] Nuevo conocimiento guardado: ${e}`))}catch{}}calculateDomHash(e){let i=e.map(a=>`${a.tag}-${a.semanticLabel}`).join("|");return Ae.createHash("sha256").update(i).digest("hex")}};var ae=()=>process.env.ARCALITY_API_URL?`${process.env.ARCALITY_API_URL}/api/v1`:null,re=()=>process.env.ARCALITY_API_KEY||"",ce=()=>process.env.ARCALITY_PROJECT_ID||"",We="00000000-0000-0000-0000-000000000000";function ge(){let o=ae(),e=re(),i=ce();return!(!o||!e||!i||i===We)}async function le(o){if(!ge())return null;try{let e=await fetch(`${ae()}/portal/rules`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":re()},body:JSON.stringify({project_id:ce(),rule_type:o.rule_type,title:o.title.substring(0,100),description:o.description.substring(0,500),severity:o.severity??"important",source:"agent"})});if(!e.ok)return console.warn(`[CollectiveMemory] pushRule HTTP ${e.status}: ${await e.text().catch(()=>"")}`),null;let i=await e.json();return console.log(`\x1B[32m[CollectiveMemory] \u2705 Regla guardada: "${o.title}"\x1B[0m`),i.id??null}catch(e){return console.warn(`[CollectiveMemory] pushRule error: ${e?.message}`),null}}async function ve(o){if(!ge()||o.existingFieldIds?.includes(o.field_identifier))return null;try{let e=await fetch(`${ae()}/portal/fields`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":re()},body:JSON.stringify({project_id:ce(),page_id:o.page_id??null,field_identifier:o.field_identifier.substring(0,100),field_type:o.field_type??"text",is_required:o.is_required??!1,validation_rule:o.validation_rule?.substring(0,300)??null,source:"agent"})});return e.ok?(await e.json()).id??null:null}catch{return null}}async function Ie(o){if(!ge())return null;try{let e=await fetch(`${ae()}/portal/knowledge`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":re()},body:JSON.stringify({project_id:ce(),doc_type:o.doc_type,title:o.title.substring(0,150),content:o.content.substring(0,500),source:"agent"})});if(!e.ok)return console.warn(`[CollectiveMemory] pushKnowledge HTTP ${e.status}: ${await e.text().catch(()=>"")}`),null;let i=await e.json();return console.log(`\x1B[32m[CollectiveMemory] \u2705 Conocimiento guardado: "${o.title}"\x1B[0m`),i.id??null}catch(e){return console.warn(`[CollectiveMemory] pushKnowledge error: ${e?.message}`),null}}async function Ee(o){if(!ge())return[];try{let e=await fetch(`${ae()}/portal/context?project_id=${ce()}&path=${encodeURIComponent(o)}`,{headers:{"x-api-key":re()}});return e.ok?((await e.json())?.fields??[]).map(a=>a.field_identifier??"").filter(Boolean):[]}catch{return[]}}var _e=Z(require("node:fs")),Ce=Z(require("node:path"));function xe(o){let e=Ce.default.join(o,"package.json"),i={name:"Arcality Project",version:"1.0.0",framework:"unknown",scripts:[],dependencies:[]};if(!_e.default.existsSync(e))return i;try{let a=JSON.parse(_e.default.readFileSync(e,"utf8"));i.name=a.name||i.name,i.version=a.version||i.version;let l={...a.dependencies,...a.devDependencies};i.dependencies=Object.keys(l),i.scripts=a.scripts?Object.keys(a.scripts):[],l.next?i.framework="next":l.vite?i.framework="vite":l["react-scripts"]&&(i.framework="cra")}catch{}return i}var Xe=[{name:"test_xss_injection",description:"Injects a standard XSS payload into an input field to test for reflected XSS vulnerabilities. Reports a vulnerability if an alert is triggered.",parameters:{type:"object",properties:{selector:{type:"string",description:"The CSS selector of the input field to test."}},required:["selector"]}},{name:"test_auth_bypass",description:"Attempts to directly navigate to a URL that should be protected to check for authentication bypass vulnerabilities.",parameters:{type:"object",properties:{url:{type:"string",description:"The protected URL to test."},redirectUrl:{type:"string",description:"The expected URL to be redirected to if unauthorized (e.g., '/login')."}},required:["url","redirectUrl"]}},{name:"audit_http_headers",description:"Audits the current page's HTTP response headers for common security headers like CSP, HSTS, etc.",parameters:{type:"object",properties:{}}},{name:"scan_sensitive_data_exposure",description:"Scans localStorage and sessionStorage for keys that might contain sensitive data (e.g., 'token', 'password').",parameters:{type:"object",properties:{}}}],Re=new Set,he=class{page;contextDir;testInfo;knowledgeService;lastScreenshot=null;logs=[];checkpoints=new Map;sessionStorage=new Map;constructor(e,i="out",a,l){this.page=e,this.contextDir=i,this.testInfo=a,this.knowledgeService=l||me.getInstance(),this.page.on("console",h=>{(h.type()==="error"||h.type()==="warning")&&(this.logs.push(`[${h.type().toUpperCase()}] ${h.text()}`),this.logs.length>30&&this.logs.shift())}),this.page.on("requestfailed",h=>{let w=h.failure();this.logs.push(`[NETWORK_ERROR] ${h.method()} ${h.url()} - ${w?.errorText||"Unknown error"}`),this.logs.length>30&&this.logs.shift()})}loadSkills(){try{let e="",i=0,a=process.env.ARCALITY_ROOT||ie.join(__dirname,"..",".."),l=[ie.join(a,".agent","skills"),ie.join(a,".agents","skills")];for(let h of l){if(!K.existsSync(h))continue;i===0&&(e+=`
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __copyProps = (to, from, except, desc) => {
|
|
8
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
9
|
+
for (let key of __getOwnPropNames(from))
|
|
10
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
11
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
12
|
+
}
|
|
13
|
+
return to;
|
|
14
|
+
};
|
|
15
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
16
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
17
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
18
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
19
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
20
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
21
|
+
mod
|
|
22
|
+
));
|
|
23
|
+
|
|
24
|
+
// tests/_helpers/agentic-runner.spec.ts
|
|
25
|
+
var import_test = require("@playwright/test");
|
|
26
|
+
|
|
27
|
+
// tests/_helpers/qa-tools.ts
|
|
28
|
+
var validateElementStateTool = {
|
|
29
|
+
name: "validate_element_state",
|
|
30
|
+
description: "QA Skill: Validate that an element meets expected conditions (exists, visible, enabled, disabled, contains text, has value). Use this for assertions during test execution WITHOUT executing actions. This is the KEY tool for verifying application state.",
|
|
31
|
+
input_schema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
idx: {
|
|
35
|
+
type: "number",
|
|
36
|
+
description: "The IDX of the element to validate"
|
|
37
|
+
},
|
|
38
|
+
validations: {
|
|
39
|
+
type: "array",
|
|
40
|
+
description: "List of validations to perform on this element",
|
|
41
|
+
items: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
type: {
|
|
45
|
+
enum: ["exists", "visible", "enabled", "disabled", "contains_text", "has_value", "is_empty", "has_class"],
|
|
46
|
+
description: "Type of validation to perform"
|
|
47
|
+
},
|
|
48
|
+
expected_value: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Expected value (required for: contains_text, has_value, has_class)"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
required: ["type"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
required: ["idx", "validations"]
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var extractTableDataTool = {
|
|
61
|
+
name: "extract_table_data",
|
|
62
|
+
description: "QA Skill: Extract structured data from tables or lists on the page. Use this to validate datasets, counts, or specific values. Returns an array of objects representing rows with their data.",
|
|
63
|
+
input_schema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
table_identifier: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Text content near the table, aria-label, or distinctive text that identifies the table (e.g., 'Timesheet - Hoy', 'Listado de pendientes')"
|
|
69
|
+
},
|
|
70
|
+
max_rows: {
|
|
71
|
+
type: "number",
|
|
72
|
+
description: "Maximum number of rows to extract (default: 50)",
|
|
73
|
+
default: 50
|
|
74
|
+
},
|
|
75
|
+
filter_column: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Optional: Column name to filter by"
|
|
78
|
+
},
|
|
79
|
+
filter_value: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Optional: Value to match in the filter column"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
required: ["table_identifier"]
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var captureConsoleErrorsTool = {
|
|
88
|
+
name: "capture_console_errors",
|
|
89
|
+
description: "QA Skill: Capture JavaScript console errors, warnings, and network failures that occurred during the test. Use this to detect client-side bugs that are not visible in the UI. This is CRITICAL for detecting hidden issues.",
|
|
90
|
+
input_schema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
severity_filter: {
|
|
94
|
+
enum: ["all", "error", "warning"],
|
|
95
|
+
description: "Filter by severity level (default: error)",
|
|
96
|
+
default: "error"
|
|
97
|
+
},
|
|
98
|
+
include_network_errors: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
description: "Include failed network requests (HTTP errors)",
|
|
101
|
+
default: true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var assertUrlPatternTool = {
|
|
107
|
+
name: "assert_url_pattern",
|
|
108
|
+
description: "QA Skill: Validate that the current URL matches (or doesn't match) an expected pattern. Use this to ensure navigation worked correctly or to verify you're on the expected page.",
|
|
109
|
+
input_schema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
pattern: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "URL pattern to match (can be a substring like '/welcome' or a full URL)"
|
|
115
|
+
},
|
|
116
|
+
should_match: {
|
|
117
|
+
type: "boolean",
|
|
118
|
+
description: "True if URL should contain the pattern, false if it should NOT",
|
|
119
|
+
default: true
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
required: ["pattern"]
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var measurePagePerformanceTool = {
|
|
126
|
+
name: "measure_page_performance",
|
|
127
|
+
description: "QA Skill: Measure page load time, network requests, and performance metrics (FCP, DOM loaded, total load). Use this to validate performance requirements or SLAs. Returns measured values and PASS/FAIL against optional thresholds.",
|
|
128
|
+
input_schema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
metrics_to_capture: {
|
|
132
|
+
type: "array",
|
|
133
|
+
items: {
|
|
134
|
+
enum: ["load_time", "dom_content_loaded", "first_contentful_paint", "network_requests", "resource_size"]
|
|
135
|
+
},
|
|
136
|
+
description: "Metrics to measure (default: all available)"
|
|
137
|
+
},
|
|
138
|
+
thresholds: {
|
|
139
|
+
type: "object",
|
|
140
|
+
description: "Optional thresholds for PASS/FAIL evaluation (e.g., {load_time_ms: 3000, dom_content_loaded_ms: 2000})",
|
|
141
|
+
properties: {
|
|
142
|
+
load_time_ms: { type: "number", description: "Maximum acceptable page load time in ms" },
|
|
143
|
+
dom_content_loaded_ms: { type: "number", description: "Maximum acceptable DOM content loaded time in ms" },
|
|
144
|
+
first_contentful_paint_ms: { type: "number", description: "Maximum acceptable FCP in ms" },
|
|
145
|
+
max_network_requests: { type: "number", description: "Maximum number of network requests allowed" },
|
|
146
|
+
max_resource_size_kb: { type: "number", description: "Maximum total resource size in KB" }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var saveNavigationCheckpointTool = {
|
|
153
|
+
name: "save_navigation_checkpoint",
|
|
154
|
+
description: "QA Skill: Save the current page state (URL, cookies, localStorage, sessionStorage) as a named checkpoint. Use this before testing multiple paths from the same starting point, so you can restore later without repeating setup steps.",
|
|
155
|
+
input_schema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
checkpoint_name: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "Unique name for this checkpoint (e.g., 'form_filled', 'logged_in', 'before_submit')"
|
|
161
|
+
},
|
|
162
|
+
include_storage: {
|
|
163
|
+
type: "boolean",
|
|
164
|
+
description: "Save localStorage and sessionStorage (default: true)",
|
|
165
|
+
default: true
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
required: ["checkpoint_name"]
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var restoreNavigationCheckpointTool = {
|
|
172
|
+
name: "restore_navigation_checkpoint",
|
|
173
|
+
description: "QA Skill: Restore a previously saved checkpoint to return to that page state. This navigates back to the saved URL and restores cookies/storage. Use this to test alternative paths from the same starting point without re-running the entire flow.",
|
|
174
|
+
input_schema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
checkpoint_name: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "Name of the checkpoint to restore (must have been saved previously)"
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
required: ["checkpoint_name"]
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var createTestEvidenceTool = {
|
|
186
|
+
name: "create_test_evidence",
|
|
187
|
+
description: "QA Skill: Create visual test evidence (screenshots or annotated screenshots) and save them to disk. Use 'screenshot' for a plain capture, 'annotated_screenshot' to highlight specific elements with colored borders and labels, or 'full_page_screenshot' for a full-page capture. Evidence files are saved for bug reports and test documentation.",
|
|
188
|
+
input_schema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
evidence_type: {
|
|
192
|
+
enum: ["screenshot", "annotated_screenshot", "full_page_screenshot"],
|
|
193
|
+
description: "Type of evidence to create"
|
|
194
|
+
},
|
|
195
|
+
annotations: {
|
|
196
|
+
type: "array",
|
|
197
|
+
description: "Elements to highlight with labels (only for 'annotated_screenshot')",
|
|
198
|
+
items: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
idx: { type: "number", description: "IDX of the element to highlight" },
|
|
202
|
+
label: { type: "string", description: "Annotation text to display near the element" },
|
|
203
|
+
color: { type: "string", description: "Highlight color (default: red)", default: "red" }
|
|
204
|
+
},
|
|
205
|
+
required: ["idx", "label"]
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
save_as: {
|
|
209
|
+
type: "string",
|
|
210
|
+
description: "Filename for the evidence (without extension, .png will be added). Example: 'bug_save_button_disabled'"
|
|
211
|
+
},
|
|
212
|
+
description: {
|
|
213
|
+
type: "string",
|
|
214
|
+
description: "Description of what this evidence proves or highlights"
|
|
215
|
+
},
|
|
216
|
+
finish_run: {
|
|
217
|
+
type: "boolean",
|
|
218
|
+
description: "If true, immediately ends the mission after saving evidence (saves tokens). Use this for final success evidence."
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
required: ["evidence_type", "save_as"]
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var sendKeyboardEventTool = {
|
|
225
|
+
name: "send_keyboard_event",
|
|
226
|
+
description: "QA Skill: Send a keyboard event to the page or a specific element. Use this for: pressing Escape to close modals/popups, pressing Tab to navigate between form fields, pressing Enter to submit or confirm, pressing Arrow keys to navigate within date/time pickers, dropdowns, sliders, or any custom component that responds to keyboard input. This is CRITICAL for native browser controls (time pickers, date pickers, color pickers) that do not respond well to 'fill'.",
|
|
227
|
+
input_schema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
key: {
|
|
231
|
+
type: "string",
|
|
232
|
+
description: "The key to press. Supported values: 'Escape', 'Enter', 'Tab', 'Backspace', 'Delete', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Space', or any single character like 'a', '1', etc. For key combinations use '+' separator: 'Control+a', 'Shift+Tab'."
|
|
233
|
+
},
|
|
234
|
+
idx: {
|
|
235
|
+
type: "number",
|
|
236
|
+
description: "Optional: IDX of the element to focus before sending the key. If not provided, the key is sent to whatever element currently has focus."
|
|
237
|
+
},
|
|
238
|
+
repeat: {
|
|
239
|
+
type: "number",
|
|
240
|
+
description: "Number of times to press the key (default: 1). Useful for ArrowUp/ArrowDown in time pickers.",
|
|
241
|
+
default: 1
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
required: ["key"]
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var interactNativeControlTool = {
|
|
248
|
+
name: "interact_native_control",
|
|
249
|
+
description: "QA Skill: Interact with native HTML controls that standard click/fill may not handle correctly. This includes: <input type='time'>, <input type='date'>, <input type='datetime-local'>, <input type='color'>, <input type='range'>, <select> with optgroup, and contenteditable elements. This tool uses Playwright's specialized methods (selectOption, fill with format coercion, press sequences) to reliably set values. ALWAYS prefer this tool over manual fill for time/date inputs.",
|
|
250
|
+
input_schema: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
idx: {
|
|
254
|
+
type: "number",
|
|
255
|
+
description: "The IDX of the native control element"
|
|
256
|
+
},
|
|
257
|
+
control_type: {
|
|
258
|
+
enum: ["time", "date", "datetime", "color", "range", "select", "contenteditable"],
|
|
259
|
+
description: "The type of native control"
|
|
260
|
+
},
|
|
261
|
+
value: {
|
|
262
|
+
type: "string",
|
|
263
|
+
description: "The value to set. Formats: time='HH:mm' or 'HH:mm:ss', date='YYYY-MM-DD', datetime='YYYY-MM-DDTHH:mm', color='#rrggbb', range='number', select='option text or value', contenteditable='text to type'."
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
required: ["idx", "control_type", "value"]
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var scrollPageTool = {
|
|
270
|
+
name: "scroll_page",
|
|
271
|
+
description: "QA Skill: Scroll the page or a specific container to reveal elements that are outside the current viewport. Use this when: you cannot find an expected element in CURRENT COMPONENTS (it might be below the fold), a form has many fields and you need to see more, or you need to scroll to a specific element. After scrolling, the page state will be refreshed in the next turn.",
|
|
272
|
+
input_schema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
direction: {
|
|
276
|
+
enum: ["up", "down", "left", "right", "top", "bottom"],
|
|
277
|
+
description: "Direction to scroll. 'top' and 'bottom' scroll all the way to the start/end."
|
|
278
|
+
},
|
|
279
|
+
amount: {
|
|
280
|
+
type: "number",
|
|
281
|
+
description: "Pixels to scroll (default: 500). Ignored for 'top' and 'bottom' directions.",
|
|
282
|
+
default: 500
|
|
283
|
+
},
|
|
284
|
+
idx: {
|
|
285
|
+
type: "number",
|
|
286
|
+
description: "Optional: IDX of a scrollable container. If omitted, scrolls the main page."
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
required: ["direction"]
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
var qaAdvancedTools = [
|
|
293
|
+
validateElementStateTool,
|
|
294
|
+
extractTableDataTool,
|
|
295
|
+
captureConsoleErrorsTool,
|
|
296
|
+
assertUrlPatternTool,
|
|
297
|
+
measurePagePerformanceTool,
|
|
298
|
+
saveNavigationCheckpointTool,
|
|
299
|
+
restoreNavigationCheckpointTool,
|
|
300
|
+
createTestEvidenceTool,
|
|
301
|
+
sendKeyboardEventTool,
|
|
302
|
+
interactNativeControlTool,
|
|
303
|
+
scrollPageTool
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
// tests/_helpers/qa-security-tools.ts
|
|
307
|
+
async function audit_http_headers(page) {
|
|
308
|
+
console.log(`[QA-SEC] Auditing HTTP Headers for: ${page.url()}`);
|
|
309
|
+
const response = await page.reload({ waitUntil: "domcontentloaded" });
|
|
310
|
+
const headers = response?.headers();
|
|
311
|
+
const vulnerabilities = [];
|
|
312
|
+
const securityHeaders = {
|
|
313
|
+
"Content-Security-Policy": {
|
|
314
|
+
severity: "Medium",
|
|
315
|
+
remediation: "Implement a strict Content-Security-Policy (CSP) to mitigate XSS and other injection attacks. Start with a restrictive policy and gradually allow trusted sources.",
|
|
316
|
+
impact: "If an injection issue exists elsewhere in the application, the lack of CSP may increase exploitability and impact.",
|
|
317
|
+
cwe: "CWE-16"
|
|
318
|
+
},
|
|
319
|
+
"Strict-Transport-Security": {
|
|
320
|
+
severity: "Medium",
|
|
321
|
+
remediation: "Implement HTTP Strict Transport Security (HSTS) to force browsers to communicate over HTTPS, preventing downgrade attacks.",
|
|
322
|
+
impact: "Man-in-the-middle attacks could downgrade the connection to HTTP, allowing traffic interception.",
|
|
323
|
+
cwe: "CWE-319"
|
|
324
|
+
},
|
|
325
|
+
"X-Content-Type-Options": {
|
|
326
|
+
severity: "Low",
|
|
327
|
+
remediation: "Set `X-Content-Type-Options: nosniff` to prevent browsers from MIME-sniffing a response away from the declared content-type.",
|
|
328
|
+
impact: "Browsers may interpret non-executable MIME types as executable, leading to potential XSS.",
|
|
329
|
+
cwe: "CWE-430"
|
|
330
|
+
},
|
|
331
|
+
"X-Frame-Options": {
|
|
332
|
+
severity: "Medium",
|
|
333
|
+
remediation: "Set `X-Frame-Options: SAMEORIGIN` or `DENY` to protect against clickjacking attacks by preventing the page from being loaded in an iframe on other domains.",
|
|
334
|
+
impact: "Attackers could trick users into clicking visually hidden elements, causing unintended actions (Clickjacking).",
|
|
335
|
+
cwe: "CWE-1021"
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
for (const [header, info] of Object.entries(securityHeaders)) {
|
|
339
|
+
if (!headers || !Object.keys(headers).find((h) => h.toLowerCase() === header.toLowerCase())) {
|
|
340
|
+
vulnerabilities.push({
|
|
341
|
+
type: "security_misconfiguration",
|
|
342
|
+
category: "Missing Security Header",
|
|
343
|
+
subcategory: header,
|
|
344
|
+
severity: info.severity,
|
|
345
|
+
confidence: "High",
|
|
346
|
+
exploitability: "not_confirmed",
|
|
347
|
+
status: "detected",
|
|
348
|
+
description: `The '${header}' HTTP header is missing. This header is crucial for defending against various web attacks.`,
|
|
349
|
+
impact: info.impact,
|
|
350
|
+
evidence: {
|
|
351
|
+
url: page.url(),
|
|
352
|
+
missingHeader: header
|
|
353
|
+
},
|
|
354
|
+
reproductionSteps: [
|
|
355
|
+
`Navigate to ${page.url()}`,
|
|
356
|
+
`Inspect the response headers.`,
|
|
357
|
+
`Verify that ${header} is absent.`
|
|
358
|
+
],
|
|
359
|
+
remediation: info.remediation,
|
|
360
|
+
classification: {
|
|
361
|
+
owaspTop10: "A05:2021 Security Misconfiguration",
|
|
362
|
+
cwe: info.cwe
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
console.log(`[QA-SEC] Found ${vulnerabilities.length} missing security headers.`);
|
|
368
|
+
return vulnerabilities;
|
|
369
|
+
}
|
|
370
|
+
async function scan_sensitive_data_exposure(page) {
|
|
371
|
+
console.log(`[QA-SEC] Scanning localStorage and sessionStorage for sensitive data.`);
|
|
372
|
+
const vulnerabilities = [];
|
|
373
|
+
const sensitiveKeywords = ["token", "password", "secret", "key", "jwt", "auth"];
|
|
374
|
+
const storages = {
|
|
375
|
+
localStorage: await page.evaluate(() => Object.entries(window.localStorage)),
|
|
376
|
+
sessionStorage: await page.evaluate(() => Object.entries(window.sessionStorage))
|
|
377
|
+
};
|
|
378
|
+
for (const [storageType, items] of Object.entries(storages)) {
|
|
379
|
+
for (const [key, value] of items) {
|
|
380
|
+
if (sensitiveKeywords.some((keyword) => key.toLowerCase().includes(keyword))) {
|
|
381
|
+
vulnerabilities.push({
|
|
382
|
+
type: "potential_issue",
|
|
383
|
+
category: "Sensitive Data Exposure",
|
|
384
|
+
subcategory: "Browser Storage",
|
|
385
|
+
severity: "Medium",
|
|
386
|
+
confidence: "Medium",
|
|
387
|
+
exploitability: "unknown",
|
|
388
|
+
status: "detected",
|
|
389
|
+
description: `Potentially sensitive data was found in ${storageType}. Storing sensitive information like tokens or keys in browser storage is insecure.`,
|
|
390
|
+
impact: "If an attacker discovers an XSS vulnerability, they can read localStorage/sessionStorage and exfiltrate secrets.",
|
|
391
|
+
evidence: {
|
|
392
|
+
storage: storageType,
|
|
393
|
+
key,
|
|
394
|
+
valuePreview: value ? `${value.substring(0, 10)}...` : "(empty)"
|
|
395
|
+
},
|
|
396
|
+
reproductionSteps: [
|
|
397
|
+
`Open Developer Tools on ${page.url()}`,
|
|
398
|
+
`Check ${storageType} for the key '${key}'.`
|
|
399
|
+
],
|
|
400
|
+
remediation: "Avoid storing sensitive data in browser storage. Use secure, HttpOnly cookies for session tokens. If you must store data, encrypt it first.",
|
|
401
|
+
classification: {
|
|
402
|
+
owaspTop10: "A04:2021 Insecure Design",
|
|
403
|
+
cwe: "CWE-312"
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
console.log(`[QA-SEC] Found ${vulnerabilities.length} instances of sensitive data in browser storage.`);
|
|
410
|
+
return vulnerabilities;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// tests/_helpers/ai-agent-helper.ts
|
|
414
|
+
var fs = __toESM(require("fs"));
|
|
415
|
+
var path = __toESM(require("path"));
|
|
416
|
+
|
|
417
|
+
// src/KnowledgeService.ts
|
|
418
|
+
var crypto = __toESM(require("crypto"));
|
|
419
|
+
var import_chalk = __toESM(require("chalk"));
|
|
420
|
+
var KnowledgeService = class _KnowledgeService {
|
|
421
|
+
static instance;
|
|
422
|
+
apiBase;
|
|
423
|
+
apiKey;
|
|
424
|
+
projectId = null;
|
|
425
|
+
constructor() {
|
|
426
|
+
this.apiBase = process.env.ARCALITY_API_URL || "https://arcalityqadev.arcadial.lat";
|
|
427
|
+
this.apiKey = process.env.ARCALITY_API_KEY || "";
|
|
428
|
+
this.projectId = process.env.ARCALITY_PROJECT_ID || null;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Fetch wrapper with a 10-second timeout to prevent hanging.
|
|
432
|
+
* All KnowledgeService calls are telemetry/secondary — they must NEVER block the agent.
|
|
433
|
+
*/
|
|
434
|
+
async fetchWithTimeout(url, options = {}, timeoutMs = 1e4) {
|
|
435
|
+
const controller = new AbortController();
|
|
436
|
+
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
437
|
+
try {
|
|
438
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
439
|
+
clearTimeout(id);
|
|
440
|
+
return res;
|
|
441
|
+
} catch (err) {
|
|
442
|
+
clearTimeout(id);
|
|
443
|
+
throw err;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
static getInstance() {
|
|
447
|
+
if (!_KnowledgeService.instance) {
|
|
448
|
+
_KnowledgeService.instance = new _KnowledgeService();
|
|
449
|
+
}
|
|
450
|
+
return _KnowledgeService.instance;
|
|
451
|
+
}
|
|
452
|
+
setProjectId(id) {
|
|
453
|
+
this.projectId = id;
|
|
454
|
+
}
|
|
455
|
+
getProjectId() {
|
|
456
|
+
if (!this.projectId || this.projectId === "undefined" || this.projectId === "null") {
|
|
457
|
+
this.projectId = process.env.ARCALITY_PROJECT_ID || null;
|
|
458
|
+
}
|
|
459
|
+
return this.projectId;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* 1. Gestión de Proyectos
|
|
463
|
+
*/
|
|
464
|
+
async getProjects() {
|
|
465
|
+
try {
|
|
466
|
+
const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/projects`, {
|
|
467
|
+
headers: { "x-api-key": this.apiKey }
|
|
468
|
+
});
|
|
469
|
+
if (!res.ok)
|
|
470
|
+
return [];
|
|
471
|
+
const data = await res.json();
|
|
472
|
+
return data.projects || [];
|
|
473
|
+
} catch (e) {
|
|
474
|
+
console.warn(import_chalk.default.yellow(`[KnowledgeService] Error al obtener proyectos: ${e.message}`));
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async createProject(name, baseUrl) {
|
|
479
|
+
try {
|
|
480
|
+
const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/projects`, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: {
|
|
483
|
+
"Content-Type": "application/json",
|
|
484
|
+
"x-api-key": this.apiKey
|
|
485
|
+
},
|
|
486
|
+
body: JSON.stringify({ name, base_url: baseUrl })
|
|
487
|
+
});
|
|
488
|
+
if (!res.ok)
|
|
489
|
+
return null;
|
|
490
|
+
return await res.json();
|
|
491
|
+
} catch (e) {
|
|
492
|
+
console.error(import_chalk.default.red(`[KnowledgeService] Fall\xF3 creaci\xF3n de proyecto: ${e.message}`));
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 2. Ingesta (Post-Percept)
|
|
498
|
+
*/
|
|
499
|
+
async ingest(payload) {
|
|
500
|
+
if (!payload.project_id || payload.project_id === "undefined") {
|
|
501
|
+
console.warn(import_chalk.default.yellow(`[Knowledge] Ingesta cancelada: project_id es inv\xE1lido.`));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/portal/ingest`, {
|
|
506
|
+
method: "POST",
|
|
507
|
+
headers: {
|
|
508
|
+
"Content-Type": "application/json",
|
|
509
|
+
"x-api-key": this.apiKey
|
|
510
|
+
},
|
|
511
|
+
body: JSON.stringify(payload)
|
|
512
|
+
});
|
|
513
|
+
if (res.status === 200) {
|
|
514
|
+
const data = await res.json();
|
|
515
|
+
if (data.status === "skipped_duplicate_dom") {
|
|
516
|
+
} else {
|
|
517
|
+
console.log(import_chalk.default.cyan(`[Knowledge] Ingesta exitosa (Project: ${payload.project_id})`));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch (e) {
|
|
521
|
+
console.warn(import_chalk.default.yellow(`[Knowledge] Error en ingesta: ${e.message}`));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* 3. Contexto (Pre-Reasoning)
|
|
526
|
+
*/
|
|
527
|
+
async getContext(url) {
|
|
528
|
+
const pid = this.getProjectId();
|
|
529
|
+
if (!pid || pid === "undefined")
|
|
530
|
+
return null;
|
|
531
|
+
try {
|
|
532
|
+
const pathUrl = new URL(url).pathname;
|
|
533
|
+
const res = await this.fetchWithTimeout(
|
|
534
|
+
`${this.apiBase}/api/v1/portal/context?project_id=${pid}&path=${encodeURIComponent(pathUrl)}`,
|
|
535
|
+
{ headers: { "x-api-key": this.apiKey } }
|
|
536
|
+
);
|
|
537
|
+
if (!res.ok)
|
|
538
|
+
return null;
|
|
539
|
+
return await res.json();
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.warn(import_chalk.default.yellow(`[Knowledge] Error al obtener contexto: ${e.message}`));
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* 4. Aprendizaje (portal_domain_rules)
|
|
547
|
+
*/
|
|
548
|
+
async saveRule(title, description, severity = "important", pageUrl) {
|
|
549
|
+
const pid = this.getProjectId();
|
|
550
|
+
if (!pid || pid === "undefined")
|
|
551
|
+
return;
|
|
552
|
+
try {
|
|
553
|
+
const pathUrl = pageUrl ? new URL(pageUrl).pathname : null;
|
|
554
|
+
await fetch(`${this.apiBase}/api/v1/portal/rules`, {
|
|
555
|
+
method: "POST",
|
|
556
|
+
headers: {
|
|
557
|
+
"Content-Type": "application/json",
|
|
558
|
+
"x-api-key": this.apiKey
|
|
559
|
+
},
|
|
560
|
+
body: JSON.stringify({
|
|
561
|
+
project_id: pid,
|
|
562
|
+
rule_type: "UI_UX",
|
|
563
|
+
// Default según especificación
|
|
564
|
+
title,
|
|
565
|
+
description,
|
|
566
|
+
severity
|
|
567
|
+
})
|
|
568
|
+
});
|
|
569
|
+
console.log(import_chalk.default.green(`[Knowledge] Nueva regla registrada: ${title}`));
|
|
570
|
+
} catch (e) {
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* 5. Catálogo de Campos (portal_field_catalog)
|
|
575
|
+
*/
|
|
576
|
+
async saveField(identifier, type, isRequired) {
|
|
577
|
+
const pid = this.getProjectId();
|
|
578
|
+
if (!pid || pid === "undefined")
|
|
579
|
+
return;
|
|
580
|
+
try {
|
|
581
|
+
await fetch(`${this.apiBase}/api/v1/portal/fields`, {
|
|
582
|
+
method: "POST",
|
|
583
|
+
headers: {
|
|
584
|
+
"Content-Type": "application/json",
|
|
585
|
+
"x-api-key": this.apiKey
|
|
586
|
+
},
|
|
587
|
+
body: JSON.stringify({
|
|
588
|
+
project_id: pid,
|
|
589
|
+
field_identifier: identifier,
|
|
590
|
+
field_type: type,
|
|
591
|
+
is_required: isRequired,
|
|
592
|
+
source: "agent_auto"
|
|
593
|
+
})
|
|
594
|
+
});
|
|
595
|
+
console.log(import_chalk.default.green(`[Knowledge] Nuevo campo catalogado: ${identifier}`));
|
|
596
|
+
} catch (e) {
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* 6. Conocimiento General (Documentación)
|
|
601
|
+
*/
|
|
602
|
+
async saveKnowledge(title, content, docType = "PROCESS") {
|
|
603
|
+
const pid = this.getProjectId();
|
|
604
|
+
if (!pid || pid === "undefined")
|
|
605
|
+
return;
|
|
606
|
+
try {
|
|
607
|
+
await fetch(`${this.apiBase}/api/v1/portal/knowledge`, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
headers: {
|
|
610
|
+
"Content-Type": "application/json",
|
|
611
|
+
"x-api-key": this.apiKey
|
|
612
|
+
},
|
|
613
|
+
body: JSON.stringify({
|
|
614
|
+
project_id: pid,
|
|
615
|
+
doc_type: docType,
|
|
616
|
+
title,
|
|
617
|
+
content,
|
|
618
|
+
source: "agent_auto"
|
|
619
|
+
})
|
|
620
|
+
});
|
|
621
|
+
console.log(import_chalk.default.green(`[Knowledge] Nuevo conocimiento guardado: ${title}`));
|
|
622
|
+
} catch (e) {
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Utilidad: Calcular Hash para Ingesta
|
|
627
|
+
*/
|
|
628
|
+
calculateDomHash(components) {
|
|
629
|
+
const fingerprint = components.map((c) => `${c.tag}-${c.semanticLabel}`).join("|");
|
|
630
|
+
return crypto.createHash("sha256").update(fingerprint).digest("hex");
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// src/services/collectiveMemoryService.ts
|
|
635
|
+
var getBase = () => process.env.ARCALITY_API_URL ? `${process.env.ARCALITY_API_URL}/api/v1` : null;
|
|
636
|
+
var getKey = () => process.env.ARCALITY_API_KEY || "";
|
|
637
|
+
var getPid = () => process.env.ARCALITY_PROJECT_ID || "";
|
|
638
|
+
var EMPTY_GUID = "00000000-0000-0000-0000-000000000000";
|
|
639
|
+
function isConfigured() {
|
|
640
|
+
const base = getBase();
|
|
641
|
+
const key = getKey();
|
|
642
|
+
const pid = getPid();
|
|
643
|
+
if (!base || !key || !pid || pid === EMPTY_GUID) {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
async function pushRule(rule) {
|
|
649
|
+
if (!isConfigured())
|
|
650
|
+
return null;
|
|
651
|
+
try {
|
|
652
|
+
const res = await fetch(`${getBase()}/portal/rules`, {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers: {
|
|
655
|
+
"Content-Type": "application/json",
|
|
656
|
+
"x-api-key": getKey()
|
|
657
|
+
},
|
|
658
|
+
body: JSON.stringify({
|
|
659
|
+
project_id: getPid(),
|
|
660
|
+
rule_type: rule.rule_type,
|
|
661
|
+
title: rule.title.substring(0, 100),
|
|
662
|
+
description: rule.description.substring(0, 500),
|
|
663
|
+
severity: rule.severity ?? "important",
|
|
664
|
+
source: "agent"
|
|
665
|
+
})
|
|
666
|
+
});
|
|
667
|
+
if (!res.ok) {
|
|
668
|
+
console.warn(`[CollectiveMemory] pushRule HTTP ${res.status}: ${await res.text().catch(() => "")}`);
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
const data = await res.json();
|
|
672
|
+
console.log(`\x1B[32m[CollectiveMemory] \u2705 Regla guardada: "${rule.title}"\x1B[0m`);
|
|
673
|
+
return data.id ?? null;
|
|
674
|
+
} catch (err) {
|
|
675
|
+
console.warn(`[CollectiveMemory] pushRule error: ${err?.message}`);
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function pushField(field) {
|
|
680
|
+
if (!isConfigured())
|
|
681
|
+
return null;
|
|
682
|
+
if (field.existingFieldIds?.includes(field.field_identifier)) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const res = await fetch(`${getBase()}/portal/fields`, {
|
|
687
|
+
method: "POST",
|
|
688
|
+
headers: {
|
|
689
|
+
"Content-Type": "application/json",
|
|
690
|
+
"x-api-key": getKey()
|
|
691
|
+
},
|
|
692
|
+
body: JSON.stringify({
|
|
693
|
+
project_id: getPid(),
|
|
694
|
+
page_id: field.page_id ?? null,
|
|
695
|
+
field_identifier: field.field_identifier.substring(0, 100),
|
|
696
|
+
field_type: field.field_type ?? "text",
|
|
697
|
+
is_required: field.is_required ?? false,
|
|
698
|
+
validation_rule: field.validation_rule?.substring(0, 300) ?? null,
|
|
699
|
+
source: "agent"
|
|
700
|
+
})
|
|
701
|
+
});
|
|
702
|
+
if (!res.ok) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
const data = await res.json();
|
|
706
|
+
return data.id ?? null;
|
|
707
|
+
} catch (err) {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function pushKnowledge(knowledge) {
|
|
712
|
+
if (!isConfigured())
|
|
713
|
+
return null;
|
|
714
|
+
try {
|
|
715
|
+
const res = await fetch(`${getBase()}/portal/knowledge`, {
|
|
716
|
+
method: "POST",
|
|
717
|
+
headers: {
|
|
718
|
+
"Content-Type": "application/json",
|
|
719
|
+
"x-api-key": getKey()
|
|
720
|
+
},
|
|
721
|
+
body: JSON.stringify({
|
|
722
|
+
project_id: getPid(),
|
|
723
|
+
doc_type: knowledge.doc_type,
|
|
724
|
+
title: knowledge.title.substring(0, 150),
|
|
725
|
+
content: knowledge.content.substring(0, 500),
|
|
726
|
+
source: "agent"
|
|
727
|
+
})
|
|
728
|
+
});
|
|
729
|
+
if (!res.ok) {
|
|
730
|
+
console.warn(`[CollectiveMemory] pushKnowledge HTTP ${res.status}: ${await res.text().catch(() => "")}`);
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
const data = await res.json();
|
|
734
|
+
console.log(`\x1B[32m[CollectiveMemory] \u2705 Conocimiento guardado: "${knowledge.title}"\x1B[0m`);
|
|
735
|
+
return data.id ?? null;
|
|
736
|
+
} catch (err) {
|
|
737
|
+
console.warn(`[CollectiveMemory] pushKnowledge error: ${err?.message}`);
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function getKnownFieldIdentifiers(pathUrl) {
|
|
742
|
+
if (!isConfigured())
|
|
743
|
+
return [];
|
|
744
|
+
try {
|
|
745
|
+
const res = await fetch(
|
|
746
|
+
`${getBase()}/portal/context?project_id=${getPid()}&path=${encodeURIComponent(pathUrl)}`,
|
|
747
|
+
{ headers: { "x-api-key": getKey() } }
|
|
748
|
+
);
|
|
749
|
+
if (!res.ok)
|
|
750
|
+
return [];
|
|
751
|
+
const data = await res.json();
|
|
752
|
+
return (data?.fields ?? []).map((f) => f.field_identifier ?? "").filter(Boolean);
|
|
753
|
+
} catch {
|
|
754
|
+
return [];
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// tests/_helpers/ai-agent-helper.ts
|
|
759
|
+
var qaSecurityTools = [
|
|
760
|
+
{
|
|
761
|
+
name: "test_xss_injection",
|
|
762
|
+
description: "Injects a standard XSS payload into an input field to test for reflected XSS vulnerabilities. Reports a vulnerability if an alert is triggered.",
|
|
763
|
+
parameters: {
|
|
764
|
+
type: "object",
|
|
765
|
+
properties: {
|
|
766
|
+
selector: { type: "string", description: "The CSS selector of the input field to test." }
|
|
767
|
+
},
|
|
768
|
+
required: ["selector"]
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: "test_auth_bypass",
|
|
773
|
+
description: "Attempts to directly navigate to a URL that should be protected to check for authentication bypass vulnerabilities.",
|
|
774
|
+
parameters: {
|
|
775
|
+
type: "object",
|
|
776
|
+
properties: {
|
|
777
|
+
url: { type: "string", description: "The protected URL to test." },
|
|
778
|
+
redirectUrl: { type: "string", description: "The expected URL to be redirected to if unauthorized (e.g., '/login')." }
|
|
779
|
+
},
|
|
780
|
+
required: ["url", "redirectUrl"]
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "audit_http_headers",
|
|
785
|
+
description: "Audits the current page's HTTP response headers for common security headers like CSP, HSTS, etc.",
|
|
786
|
+
parameters: { type: "object", properties: {} }
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
name: "scan_sensitive_data_exposure",
|
|
790
|
+
description: "Scans localStorage and sessionStorage for keys that might contain sensitive data (e.g., 'token', 'password').",
|
|
791
|
+
parameters: { type: "object", properties: {} }
|
|
792
|
+
}
|
|
793
|
+
];
|
|
794
|
+
var _sessionFieldCache = /* @__PURE__ */ new Set();
|
|
795
|
+
var AIAgentHelper = class {
|
|
796
|
+
page;
|
|
797
|
+
contextDir;
|
|
798
|
+
testInfo;
|
|
799
|
+
knowledgeService;
|
|
800
|
+
lastScreenshot = null;
|
|
801
|
+
logs = [];
|
|
802
|
+
checkpoints = /* @__PURE__ */ new Map();
|
|
803
|
+
sessionStorage = /* @__PURE__ */ new Map();
|
|
804
|
+
constructor(page, contextDir = "out", testInfo, knowledgeService) {
|
|
805
|
+
this.page = page;
|
|
806
|
+
this.contextDir = contextDir;
|
|
807
|
+
this.testInfo = testInfo;
|
|
808
|
+
this.knowledgeService = knowledgeService || KnowledgeService.getInstance();
|
|
809
|
+
this.page.on("console", (msg) => {
|
|
810
|
+
if (msg.type() === "error" || msg.type() === "warning") {
|
|
811
|
+
this.logs.push(`[${msg.type().toUpperCase()}] ${msg.text()}`);
|
|
812
|
+
if (this.logs.length > 30)
|
|
813
|
+
this.logs.shift();
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
this.page.on("requestfailed", (request) => {
|
|
817
|
+
const failure = request.failure();
|
|
818
|
+
this.logs.push(`[NETWORK_ERROR] ${request.method()} ${request.url()} - ${failure?.errorText || "Unknown error"}`);
|
|
819
|
+
if (this.logs.length > 30)
|
|
820
|
+
this.logs.shift();
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
loadSkills() {
|
|
824
|
+
try {
|
|
825
|
+
let skillsContext = "";
|
|
826
|
+
let loadedCount = 0;
|
|
827
|
+
const toolsRoot = process.env.ARCALITY_ROOT || path.join(__dirname, "..", "..");
|
|
828
|
+
const possibleDirs = [
|
|
829
|
+
path.join(toolsRoot, ".agent", "skills"),
|
|
830
|
+
path.join(toolsRoot, ".agents", "skills")
|
|
831
|
+
];
|
|
832
|
+
for (const rootDir of possibleDirs) {
|
|
833
|
+
if (!fs.existsSync(rootDir))
|
|
834
|
+
continue;
|
|
835
|
+
if (loadedCount === 0) {
|
|
836
|
+
skillsContext += "\n\u{1F6E0}\uFE0F SKILLS INSTALADAS (Metodolog\xEDas activas):\n";
|
|
837
|
+
}
|
|
838
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
839
|
+
for (const entry of entries) {
|
|
840
|
+
let content = "";
|
|
841
|
+
let skillName = entry.name;
|
|
842
|
+
if (entry.isDirectory()) {
|
|
843
|
+
const skillPath = path.join(rootDir, entry.name, "SKILL.md");
|
|
844
|
+
if (fs.existsSync(skillPath)) {
|
|
845
|
+
content = fs.readFileSync(skillPath, "utf8");
|
|
846
|
+
}
|
|
847
|
+
} else if (entry.name.endsWith(".md")) {
|
|
848
|
+
const skillPath = path.join(rootDir, entry.name);
|
|
849
|
+
content = fs.readFileSync(skillPath, "utf8");
|
|
850
|
+
}
|
|
851
|
+
if (content) {
|
|
852
|
+
skillsContext += `
|
|
853
|
+
--- SKILL: ${skillName} ---
|
|
854
|
+
${content}
|
|
855
|
+
`;
|
|
856
|
+
loadedCount++;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (loadedCount > 0) {
|
|
861
|
+
console.log(`>> \u{1F4D1} ${loadedCount} habilidades QA inyectadas al cerebro.`);
|
|
862
|
+
return skillsContext;
|
|
863
|
+
}
|
|
864
|
+
} catch (e) {
|
|
865
|
+
console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error cargando habilidades: ${e.message}`);
|
|
866
|
+
}
|
|
867
|
+
return "";
|
|
868
|
+
}
|
|
869
|
+
loadMemory(prompt) {
|
|
870
|
+
try {
|
|
871
|
+
const memoryFile = path.join(this.contextDir, "memoria-agente.json");
|
|
872
|
+
if (fs.existsSync(memoryFile)) {
|
|
873
|
+
const memories = JSON.parse(fs.readFileSync(memoryFile, "utf8"));
|
|
874
|
+
const keywords = prompt.toLowerCase().split(" ").filter((w) => w.length > 4);
|
|
875
|
+
const relevantSuccess = memories.filter((m) => {
|
|
876
|
+
if (!m.success)
|
|
877
|
+
return false;
|
|
878
|
+
const memPrompt = (m.prompt || "").toLowerCase();
|
|
879
|
+
const matches = keywords.filter((kw) => memPrompt.includes(kw)).length;
|
|
880
|
+
return matches >= 3 || keywords.length > 0 && matches === keywords.length;
|
|
881
|
+
}).slice(-1);
|
|
882
|
+
const relevantFailures = memories.filter((m) => !m.success && m.error).slice(-1);
|
|
883
|
+
let memoryContext = "";
|
|
884
|
+
if (relevantSuccess.length) {
|
|
885
|
+
const best = relevantSuccess[0];
|
|
886
|
+
memoryContext += `
|
|
887
|
+
\u{1F4DA} SUCCESS GUIDANCE FOR THIS MISSION:
|
|
888
|
+
The following steps led to SUCCESS for a similar mission ("${best.prompt}"):
|
|
889
|
+
${best.steps.slice(0, 15).map((s) => ` - ${s}`).join("\n")}
|
|
11
890
|
(Use these steps as a roadmap if the current UI allows it!)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
891
|
+
`;
|
|
892
|
+
}
|
|
893
|
+
if (relevantFailures.length) {
|
|
894
|
+
memoryContext += `
|
|
895
|
+
\u26A0\uFE0F PREVIOUS FAILURES TO AVOID:
|
|
896
|
+
${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
897
|
+
`;
|
|
898
|
+
}
|
|
899
|
+
return memoryContext;
|
|
900
|
+
}
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
return "";
|
|
904
|
+
}
|
|
905
|
+
async getPageState() {
|
|
906
|
+
if (this.page.isClosed())
|
|
907
|
+
return { url: "", title: "", components: [] };
|
|
908
|
+
return this.getPageStateRefined();
|
|
909
|
+
}
|
|
910
|
+
async getPageStateRefined() {
|
|
911
|
+
const frames = this.page.frames();
|
|
912
|
+
let globalIdx = 0;
|
|
913
|
+
const allComponents = [];
|
|
914
|
+
for (let fIdx = 0; fIdx < frames.length; fIdx++) {
|
|
915
|
+
const frame = frames[fIdx];
|
|
916
|
+
if (frame.isDetached())
|
|
917
|
+
continue;
|
|
918
|
+
try {
|
|
919
|
+
const results = await frame.evaluate(({ startIdx }) => {
|
|
920
|
+
const isVisible = (el) => {
|
|
921
|
+
const style = window.getComputedStyle(el);
|
|
922
|
+
const rect = el.getBoundingClientRect();
|
|
923
|
+
const hasSize = rect.width > 1 && rect.height > 1;
|
|
924
|
+
const isNotTranslucent = parseFloat(style.opacity || "1") > 0.05;
|
|
925
|
+
const isCssVisible = style.display !== "none" && style.visibility !== "hidden";
|
|
926
|
+
return isCssVisible && isNotTranslucent && hasSize;
|
|
927
|
+
};
|
|
928
|
+
const nodes = Array.from(document.querySelectorAll('button, input, select, textarea, [role], a, h1, h2, label, span, p, i, svg, img, [class*="alert"],[class*="toast"],[class*="message"],[class*="error"],[class*="success"],[id*="alert"],[id*="toast"],[role="alert"],[role="status"],.error-message,.success-message'));
|
|
929
|
+
let localCount = 0;
|
|
930
|
+
return nodes.filter((el) => {
|
|
931
|
+
const htmlEl = el;
|
|
932
|
+
if (htmlEl.tagName.toLowerCase() === "input" && htmlEl.type === "file") {
|
|
933
|
+
return window.getComputedStyle(htmlEl).display !== "none";
|
|
934
|
+
}
|
|
935
|
+
return isVisible(htmlEl);
|
|
936
|
+
}).map((el) => {
|
|
937
|
+
const htmlEl = el;
|
|
938
|
+
const tag = htmlEl.tagName.toLowerCase();
|
|
939
|
+
const role = htmlEl.getAttribute("role") || "";
|
|
940
|
+
const text = (htmlEl.innerText || htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || "").trim().replace(/\s+/g, " ");
|
|
941
|
+
const textLower = text.toLowerCase();
|
|
942
|
+
const isCriticalFailure = (textLower.includes("error") || textLower.includes("fall\xF3") || textLower.includes("no se puede") || textLower.includes("ya existe") || textLower.includes("existe") || textLower.includes("inv\xE1lido") || textLower.includes("incorrecto") || textLower.includes("ligado") || textLower.includes("depende") || textLower.includes("duplicado") || textLower.includes("repetido")) && text.length < 200;
|
|
943
|
+
const isCriticalSuccess = (textLower.includes("exitosamente") || textLower.includes("guardado") || textLower.includes("creado") || textLower.includes("success") || textLower.includes("correctamente") && text.length < 100) && text.length < 200;
|
|
944
|
+
const isCriticalFeedback = isCriticalFailure || isCriticalSuccess;
|
|
945
|
+
if (!isCriticalFeedback && (tag === "span" || tag === "p" || tag === "label" || tag === "i" || tag === "svg" || tag === "img" || tag === "div")) {
|
|
946
|
+
const hasIdentification = htmlEl.hasAttribute("aria-label") || htmlEl.hasAttribute("title") || role || htmlEl.id;
|
|
947
|
+
if (text.length > 300 && !isCriticalFeedback && !hasIdentification)
|
|
948
|
+
return null;
|
|
949
|
+
if (text.length < 2 && !hasIdentification && tag !== "svg" && tag !== "img" && tag !== "i")
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
const idx = startIdx + localCount++;
|
|
953
|
+
htmlEl.setAttribute("agent-idx", idx.toString());
|
|
954
|
+
let name = text || htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || htmlEl.getAttribute("placeholder");
|
|
955
|
+
if (!name) {
|
|
956
|
+
const iconMatch = htmlEl.outerHTML.match(/fa-([a-z0-9-]+)|md-([a-z0-9-]+)|bi-([a-z0-9-]+)/i);
|
|
957
|
+
if (iconMatch) {
|
|
958
|
+
name = `Icon:${iconMatch[1] || iconMatch[2] || iconMatch[3]}`;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const htmlLower = htmlEl.outerHTML.toLowerCase();
|
|
962
|
+
const classLower = (htmlEl.className || "").toString().toLowerCase();
|
|
963
|
+
const isDeleteAction = htmlLower.includes("trash") || htmlLower.includes("delete") || htmlLower.includes("eliminar") || htmlLower.includes("remove");
|
|
964
|
+
const isEditAction = htmlLower.includes("edit") || htmlLower.includes("pencil") || htmlLower.includes("modify") || htmlLower.includes("editar") || htmlLower.includes("modificar") || htmlLower.includes("pen");
|
|
965
|
+
if (isDeleteAction && (!name || name.length < 3))
|
|
966
|
+
name = "[INFERRED] Eliminar";
|
|
967
|
+
if (isEditAction && (!name || name.length < 3))
|
|
968
|
+
name = "[INFERRED] Editar";
|
|
969
|
+
let type = "INFO";
|
|
970
|
+
const isErrorClass = classLower.includes("error") || classLower.includes("danger") || classLower.includes("fail");
|
|
971
|
+
const isError = isCriticalFailure || isErrorClass && text.split(" ").length > 2;
|
|
972
|
+
const isSuccess = classLower.includes("success") || isCriticalSuccess || textLower.includes("\xE9xito");
|
|
973
|
+
const isTab = role === "tab" || classLower.includes("tab") || classLower.includes("pesta\xF1a");
|
|
974
|
+
if (tag === "input" || tag === "textarea" || tag === "select")
|
|
975
|
+
type = "FIELD";
|
|
976
|
+
else if (tag === "button" || role === "button" || tag === "a" || role === "link" || isTab)
|
|
977
|
+
type = "ACTION";
|
|
978
|
+
else if (role === "option")
|
|
979
|
+
type = "OPTION";
|
|
980
|
+
else if (role === "combobox" || htmlEl.hasAttribute("aria-expanded"))
|
|
981
|
+
type = "DROPDOWN";
|
|
982
|
+
else if (isError || classLower.includes("alert") || classLower.includes("toast") || role === "status" || role === "alert" || isCriticalFeedback) {
|
|
983
|
+
type = isSuccess ? "SUCCESS_MESSAGE" : isError ? "ERROR_MESSAGE" : "MESSAGE";
|
|
984
|
+
}
|
|
985
|
+
if (type.includes("MESSAGE")) {
|
|
986
|
+
const prefix = isError ? "\u{1F6D1} [ERROR_CR\xCDTICO] " : isSuccess ? "\u2728 [\xC9XITO_DETECTADO] " : "[INFO] ";
|
|
987
|
+
name = prefix + (name || text || "Mensaje del sistema");
|
|
988
|
+
}
|
|
989
|
+
if (type === "ACTION" || type === "FIELD") {
|
|
990
|
+
name = `[${tag}] ${name || ""}`.trim();
|
|
991
|
+
if (!name || name.length < 10) {
|
|
992
|
+
const row = htmlEl.closest("tr");
|
|
993
|
+
if (row) {
|
|
994
|
+
const rowText = (row.innerText || "").split("\n")[0].substring(0, 20).trim();
|
|
995
|
+
if (rowText)
|
|
996
|
+
name += ` (${rowText})`;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (!name)
|
|
1001
|
+
name = tag;
|
|
1002
|
+
let score = 10;
|
|
1003
|
+
if (type === "MESSAGE")
|
|
1004
|
+
score = 200;
|
|
1005
|
+
else if (type === "FIELD")
|
|
1006
|
+
score = 100;
|
|
1007
|
+
else if (type === "OPTION")
|
|
1008
|
+
score = 90;
|
|
1009
|
+
else if (type === "ACTION")
|
|
1010
|
+
score = 80;
|
|
1011
|
+
else if (type === "DROPDOWN")
|
|
1012
|
+
score = 70;
|
|
1013
|
+
return {
|
|
1014
|
+
idx,
|
|
1015
|
+
type,
|
|
1016
|
+
name,
|
|
1017
|
+
value: htmlEl.value || "",
|
|
1018
|
+
frameIdx: 0,
|
|
1019
|
+
// Placeholder
|
|
1020
|
+
selector: `[agent-idx="${idx}"]`,
|
|
1021
|
+
score
|
|
1022
|
+
};
|
|
1023
|
+
}).filter(Boolean);
|
|
1024
|
+
}, { startIdx: globalIdx });
|
|
1025
|
+
results.forEach((c) => {
|
|
1026
|
+
c.frameIdx = fIdx;
|
|
1027
|
+
allComponents.push(c);
|
|
1028
|
+
});
|
|
1029
|
+
globalIdx += results.length;
|
|
1030
|
+
} catch {
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F50D} Percepci\xF3n profunda encontr\xF3 ${allComponents.length} componentes`);
|
|
1034
|
+
const sortedComponents = allComponents.sort((a, b) => b.score - a.score).slice(0, 500);
|
|
1035
|
+
const title = await this.page.title();
|
|
1036
|
+
const url = this.page.url();
|
|
1037
|
+
try {
|
|
1038
|
+
const domHash = this.knowledgeService.calculateDomHash(sortedComponents);
|
|
1039
|
+
const viewport = this.page.viewportSize() || { width: 1280, height: 720 };
|
|
1040
|
+
const projectId = this.knowledgeService.getProjectId() || process.env.ARCALITY_PROJECT_ID || "";
|
|
1041
|
+
if (projectId) {
|
|
1042
|
+
const requestPayload = {
|
|
1043
|
+
project_id: projectId,
|
|
1044
|
+
target_url: url,
|
|
1045
|
+
page_title: title,
|
|
1046
|
+
dom_hash: domHash,
|
|
1047
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
1048
|
+
components: sortedComponents.map((c) => {
|
|
1049
|
+
let compType = "INFO";
|
|
1050
|
+
if (c.type === "ACTION")
|
|
1051
|
+
compType = "ACTION";
|
|
1052
|
+
else if (c.type === "FIELD" || c.type === "OPTION" || c.type === "DROPDOWN")
|
|
1053
|
+
compType = "FIELD";
|
|
1054
|
+
else if (c.name?.toLowerCase().includes("nav") || c.type === "ACTION")
|
|
1055
|
+
compType = "NAVIGATION";
|
|
1056
|
+
return {
|
|
1057
|
+
tag: c.selector?.split("]")[0]?.replace("[", "") || "div",
|
|
1058
|
+
component_type: compType,
|
|
1059
|
+
semantic_label: c.name,
|
|
1060
|
+
text_content: c.value,
|
|
1061
|
+
is_interactive: compType === "ACTION" || compType === "FIELD",
|
|
1062
|
+
attributes: { idx: c.idx, originalType: c.type },
|
|
1063
|
+
bounding_rect: c.rect || { x: 0, y: 0, width: 0, height: 0 }
|
|
1064
|
+
};
|
|
1065
|
+
})
|
|
1066
|
+
};
|
|
1067
|
+
await this.knowledgeService.ingest(requestPayload);
|
|
1068
|
+
try {
|
|
1069
|
+
const currentPath = new URL(url).pathname;
|
|
1070
|
+
const fieldComponents = sortedComponents.filter(
|
|
1071
|
+
(c) => c.type === "FIELD" || c.type === "OPTION" || c.type === "DROPDOWN"
|
|
1072
|
+
);
|
|
1073
|
+
const newFields = fieldComponents.filter((fc) => {
|
|
1074
|
+
const identifier = (fc.name || fc.value || "").trim();
|
|
1075
|
+
return identifier.length >= 2 && !_sessionFieldCache.has(identifier);
|
|
1076
|
+
}).slice(0, 5);
|
|
1077
|
+
if (newFields.length > 0) {
|
|
1078
|
+
const existingFieldIds = await getKnownFieldIdentifiers(currentPath);
|
|
1079
|
+
for (const fc of newFields) {
|
|
1080
|
+
const identifier = (fc.name || fc.value || "").trim();
|
|
1081
|
+
_sessionFieldCache.add(identifier);
|
|
1082
|
+
const selectorLower = (fc.selector || "").toLowerCase();
|
|
1083
|
+
const nameLower = identifier.toLowerCase();
|
|
1084
|
+
let fieldType = "text";
|
|
1085
|
+
if (nameLower.includes("email") || selectorLower.includes("email"))
|
|
1086
|
+
fieldType = "email";
|
|
1087
|
+
else if (nameLower.includes("password") || selectorLower.includes("password"))
|
|
1088
|
+
fieldType = "password";
|
|
1089
|
+
else if (fc.type === "DROPDOWN" || selectorLower.includes("select"))
|
|
1090
|
+
fieldType = "select";
|
|
1091
|
+
else if (selectorLower.includes("textarea"))
|
|
1092
|
+
fieldType = "textarea";
|
|
1093
|
+
else if (selectorLower.includes("checkbox"))
|
|
1094
|
+
fieldType = "checkbox";
|
|
1095
|
+
else if (selectorLower.includes("date"))
|
|
1096
|
+
fieldType = "date";
|
|
1097
|
+
else if (selectorLower.includes("number"))
|
|
1098
|
+
fieldType = "number";
|
|
1099
|
+
pushField({
|
|
1100
|
+
field_identifier: identifier.substring(0, 100),
|
|
1101
|
+
field_type: fieldType,
|
|
1102
|
+
is_required: false,
|
|
1103
|
+
existingFieldIds
|
|
1104
|
+
}).catch(() => {
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
} catch {
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de ingesta: ${e.message}`);
|
|
1113
|
+
}
|
|
1114
|
+
return {
|
|
1115
|
+
url,
|
|
1116
|
+
title,
|
|
1117
|
+
components: sortedComponents
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Busca una acción sugerida en la memoria SIN usar tokens.
|
|
1122
|
+
* Compara URL, componentes visuales Y palabras clave del prompt.
|
|
1123
|
+
*/
|
|
1124
|
+
async getActionFromGuia(prompt, historyLength) {
|
|
1125
|
+
try {
|
|
1126
|
+
const fs3 = require("fs");
|
|
1127
|
+
const path3 = require("path");
|
|
1128
|
+
const memoryFile = path3.join(this.contextDir, "memoria-agente.json");
|
|
1129
|
+
if (!fs3.existsSync(memoryFile))
|
|
1130
|
+
return null;
|
|
1131
|
+
const memories = JSON.parse(fs3.readFileSync(memoryFile, "utf8"));
|
|
1132
|
+
const state = await this.getPageState();
|
|
1133
|
+
const currentUrl = this.page.url();
|
|
1134
|
+
const extractCriticalKeywords = (text) => {
|
|
1135
|
+
const lower = text.toLowerCase();
|
|
1136
|
+
const keywords = /* @__PURE__ */ new Set();
|
|
1137
|
+
if (lower.includes("primera"))
|
|
1138
|
+
keywords.add("primera");
|
|
1139
|
+
if (lower.includes("segunda"))
|
|
1140
|
+
keywords.add("segunda");
|
|
1141
|
+
if (lower.includes("tercera"))
|
|
1142
|
+
keywords.add("tercera");
|
|
1143
|
+
if (lower.includes("cuarta"))
|
|
1144
|
+
keywords.add("cuarta");
|
|
1145
|
+
if (lower.includes("\xFAltima"))
|
|
1146
|
+
keywords.add("\xFAltima");
|
|
1147
|
+
if (lower.includes("first"))
|
|
1148
|
+
keywords.add("primera");
|
|
1149
|
+
if (lower.includes("second"))
|
|
1150
|
+
keywords.add("segunda");
|
|
1151
|
+
if (lower.includes("third"))
|
|
1152
|
+
keywords.add("tercera");
|
|
1153
|
+
if (lower.includes("fourth"))
|
|
1154
|
+
keywords.add("cuarta");
|
|
1155
|
+
if (lower.includes("last"))
|
|
1156
|
+
keywords.add("\xFAltima");
|
|
1157
|
+
return keywords;
|
|
1158
|
+
};
|
|
1159
|
+
const currentKeywords = extractCriticalKeywords(prompt);
|
|
1160
|
+
const currentVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\xF1ade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => prompt.toLowerCase().includes(v));
|
|
1161
|
+
const matchingRun = memories.filter((m) => m.success && m.steps_data && m.steps_data[historyLength]).reverse().find((m) => {
|
|
1162
|
+
const memPrompt = (m.prompt || "").toLowerCase();
|
|
1163
|
+
const currentPrompt = prompt.toLowerCase();
|
|
1164
|
+
const words = currentPrompt.split(" ").filter((w) => w.length > 3);
|
|
1165
|
+
if (words.length === 0)
|
|
1166
|
+
return false;
|
|
1167
|
+
const matches = words.filter((w) => memPrompt.includes(w)).length;
|
|
1168
|
+
if (matches / words.length < 0.7)
|
|
1169
|
+
return false;
|
|
1170
|
+
const memVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\xF1ade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => memPrompt.includes(v));
|
|
1171
|
+
const hasSharedVerb = currentVerbs.some((cv) => memVerbs.includes(cv));
|
|
1172
|
+
const hasOppositeIntent = currentVerbs.some((v) => ["elimina", "borra", "delete"].includes(v)) && memVerbs.some((v) => ["crea", "nueva", "create", "new"].includes(v)) || currentVerbs.some((v) => ["crea", "nueva", "create", "new"].includes(v)) && memVerbs.some((v) => ["elimina", "borra", "delete"].includes(v));
|
|
1173
|
+
if (hasOppositeIntent)
|
|
1174
|
+
return false;
|
|
1175
|
+
if (!hasSharedVerb && currentVerbs.length > 0)
|
|
1176
|
+
return false;
|
|
1177
|
+
if (currentVerbs.length === 0 && memVerbs.length > 0)
|
|
1178
|
+
return false;
|
|
1179
|
+
const memKeywords = extractCriticalKeywords(m.prompt || "");
|
|
1180
|
+
for (const kw of currentKeywords) {
|
|
1181
|
+
if (!memKeywords.has(kw))
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
const memStep = m.steps_data[historyLength];
|
|
1185
|
+
if (!memStep)
|
|
1186
|
+
return false;
|
|
1187
|
+
const normalizeUrl = (u) => u.replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/");
|
|
1188
|
+
const memUrl = normalizeUrl((memStep.url || "").split("?")[0]);
|
|
1189
|
+
const currUrl = normalizeUrl(currentUrl.split("?")[0]);
|
|
1190
|
+
if (memUrl !== currUrl)
|
|
1191
|
+
return false;
|
|
1192
|
+
const foundComponent = state.components.find((c) => {
|
|
1193
|
+
const cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
|
|
1194
|
+
const cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
|
|
1195
|
+
return c.selector === memStep.selector || cleanMemName !== "" && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName));
|
|
1196
|
+
});
|
|
1197
|
+
return !!foundComponent;
|
|
1198
|
+
});
|
|
1199
|
+
if (matchingRun) {
|
|
1200
|
+
const memStep = matchingRun.steps_data[historyLength];
|
|
1201
|
+
const memCompName = memStep.componentName || "";
|
|
1202
|
+
if (memCompName.length > 80) {
|
|
1203
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa rechazada: Nombre de componente demasiado largo (${memCompName.length} chars). Delegando a IA.`);
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
if (memCompName.includes("[INFERRED]")) {
|
|
1207
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa rechazada: Componente con nombre inferido "${memCompName}". Delegando a IA.`);
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
if (!memCompName || memCompName.length < 3) {
|
|
1211
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa rechazada: Componente sin nombre claro. Delegando a IA.`);
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
const foundComponent = state.components.find((c) => {
|
|
1215
|
+
const cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
|
|
1216
|
+
const cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
|
|
1217
|
+
return c.selector === memStep.selector || cleanMemName !== "" && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName));
|
|
1218
|
+
});
|
|
1219
|
+
if (foundComponent) {
|
|
1220
|
+
return {
|
|
1221
|
+
thought: `[GU\xCDA DE \xC9XITO] Reutilizando paso maestro aprendido: ${memStep.action_data.action} en ${foundComponent.name}`,
|
|
1222
|
+
actions: [{
|
|
1223
|
+
...memStep.action_data,
|
|
1224
|
+
idx: foundComponent.idx,
|
|
1225
|
+
description: foundComponent.name,
|
|
1226
|
+
selector: foundComponent.selector,
|
|
1227
|
+
frameIdx: foundComponent.frameIdx,
|
|
1228
|
+
type: foundComponent.type
|
|
1229
|
+
}],
|
|
1230
|
+
finish: memStep.finish || false
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
}
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
async askIA(prompt, history = [], stepCount) {
|
|
1239
|
+
if (!process.env.ARCALITY_API_URL && !process.env.ANTHROPIC_API_KEY) {
|
|
1240
|
+
throw new Error("ANTHROPIC_API_KEY missing and ARCALITY_API_URL not configured");
|
|
1241
|
+
}
|
|
1242
|
+
const state = await this.getPageState();
|
|
1243
|
+
const screenshot = await this.page.screenshot({ type: "png" });
|
|
1244
|
+
const base64Img = screenshot.toString("base64");
|
|
1245
|
+
const componentsList = state.components.map(
|
|
1246
|
+
(c) => `IDX:${c.idx} | TYPE:${c.type} | NAME: "${c.name}"${c.value ? ` | VAL:"${c.value}"` : ""} `
|
|
1247
|
+
).join("\n");
|
|
1248
|
+
const rawTools = [
|
|
1249
|
+
{
|
|
1250
|
+
name: "perform_ui_actions",
|
|
1251
|
+
description: "Execute standard UI interactions (click, fill, wait) to progress in the mission.",
|
|
1252
|
+
parameters: {
|
|
1253
|
+
type: "object",
|
|
1254
|
+
properties: {
|
|
1255
|
+
thought: { type: "string", description: "Reasoning about why these actions are taken." },
|
|
1256
|
+
actions: {
|
|
1257
|
+
type: "array",
|
|
1258
|
+
items: {
|
|
1259
|
+
type: "object",
|
|
1260
|
+
properties: {
|
|
1261
|
+
idx: { type: "number", description: "The IDX from the components list." },
|
|
1262
|
+
action: { enum: ["click", "double_click", "fill", "select", "wait"] },
|
|
1263
|
+
value: { type: "string", description: "Text to fill if action is 'fill'." }
|
|
1264
|
+
},
|
|
1265
|
+
required: ["action"]
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
finish: { type: "boolean", description: "CRITICAL: Set to true only when the mission objective is fully achieved. If true, actions should ideally be empty." }
|
|
1269
|
+
},
|
|
1270
|
+
required: ["thought", "actions"]
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
name: "inspect_element_details",
|
|
1275
|
+
description: "QA Skill: Get deep technical metadata of an element (attributes, computed styles). Use this if an element is hidden, disabled, or not responding correctly.",
|
|
1276
|
+
parameters: {
|
|
1277
|
+
type: "object",
|
|
1278
|
+
properties: {
|
|
1279
|
+
idx: { type: "number", description: "The IDX of the component to research." }
|
|
1280
|
+
},
|
|
1281
|
+
required: ["idx"]
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
name: "report_application_bug",
|
|
1286
|
+
description: "QA Skill: Use this when the mission objective cannot be achieved. This includes: (1) Technical BUGS (elements not appearing), (2) BUSINESS ERRORS (validation messages like 'Conflict', 'Already exists', 'Permission denied'), or (3) Any condition that violates the user's success criteria.",
|
|
1287
|
+
parameters: {
|
|
1288
|
+
type: "object",
|
|
1289
|
+
properties: {
|
|
1290
|
+
bug_description: { type: "string", description: "Detailed description of the failure or error message." },
|
|
1291
|
+
expected_behavior: { type: "string", description: "What the mission expected (Success Criteria)." },
|
|
1292
|
+
actual_behavior: { type: "string", description: "What actually happened (Failure Criteria)." }
|
|
1293
|
+
},
|
|
1294
|
+
required: ["bug_description", "expected_behavior", "actual_behavior"]
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
name: "report_inability_to_proceed",
|
|
1299
|
+
description: "CRITICAL: Use this when you are STUCK, CONFUSED, or CANNOT find required elements after 3+ attempts. This tool allows you to gracefully admit confusion and help the user rephrase the prompt. Use when: (1) You've clicked the same element 3+ times, (2) Cannot find a specific UI element mentioned in the prompt, (3) The prompt is ambiguous or contradictory.",
|
|
1300
|
+
parameters: {
|
|
1301
|
+
type: "object",
|
|
1302
|
+
properties: {
|
|
1303
|
+
confusion_reason: {
|
|
1304
|
+
type: "string",
|
|
1305
|
+
description: "Explain why you cannot proceed (e.g., 'Cannot find Menu icon after 5 attempts', 'Stuck clicking same element repeatedly', 'Prompt asks for first but I only see second')"
|
|
1306
|
+
},
|
|
1307
|
+
attempts_summary: {
|
|
1308
|
+
type: "string",
|
|
1309
|
+
description: "Summarize what you tried (e.g., 'Clicked TAE-063 5 times, went back to list 5 times')"
|
|
1310
|
+
},
|
|
1311
|
+
prompt_suggestion: {
|
|
1312
|
+
type: "string",
|
|
1313
|
+
description: "Suggest how the user can rephrase for clarity (e.g., 'Please specify the exact text or position of the menu icon')"
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
required: ["confusion_reason", "attempts_summary", "prompt_suggestion"]
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
...qaAdvancedTools.map((t) => ({
|
|
1320
|
+
name: t.name,
|
|
1321
|
+
description: t.description,
|
|
1322
|
+
parameters: t.input_schema || t.parameters
|
|
1323
|
+
})),
|
|
1324
|
+
...qaSecurityTools.map((t) => ({
|
|
1325
|
+
name: t.name,
|
|
1326
|
+
description: t.description,
|
|
1327
|
+
parameters: t.input_schema || t.parameters
|
|
1328
|
+
}))
|
|
1329
|
+
];
|
|
1330
|
+
const credentialsContext = process.env.LOGIN_USER && process.env.LOGIN_PASSWORD ? `
|
|
1331
|
+
\u{1F511} QA CREDENTIALS (Use if login is needed):
|
|
20
1332
|
- User: ${process.env.LOGIN_USER}
|
|
21
1333
|
- Password: ${process.env.LOGIN_PASSWORD}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1334
|
+
` : "";
|
|
1335
|
+
const memoryContext = this.loadMemory(prompt);
|
|
1336
|
+
const skillsContext = this.loadSkills();
|
|
1337
|
+
let projectInfo = "";
|
|
1338
|
+
try {
|
|
1339
|
+
const { getProjectContext } = require("../src/projectInspector");
|
|
1340
|
+
const ctx = getProjectContext(process.cwd());
|
|
1341
|
+
projectInfo = `
|
|
1342
|
+
\u{1F3D7}\uFE0F PROJECT CONTEXT:
|
|
1343
|
+
- Name: ${ctx.name}
|
|
1344
|
+
- Tech Stack: ${ctx.framework}
|
|
1345
|
+
- Environment: ${process.env.NODE_ENV || "development"}
|
|
1346
|
+
- Active Config: ${process.env.ACTIVE_CONFIG || "Default"}
|
|
1347
|
+
`;
|
|
1348
|
+
} catch {
|
|
1349
|
+
}
|
|
1350
|
+
let memoryBackendContext = "";
|
|
1351
|
+
try {
|
|
1352
|
+
const contextData = await this.knowledgeService.getContext(state.url);
|
|
1353
|
+
if (contextData && (contextData.rules?.length > 0 || contextData.fields?.length > 0)) {
|
|
1354
|
+
memoryBackendContext = `
|
|
1355
|
+
\u{1F9E0} BACKEND COLLECTIVE MEMORY FOR THIS PAGE:
|
|
1356
|
+
`;
|
|
1357
|
+
if (contextData.rules?.length > 0) {
|
|
1358
|
+
memoryBackendContext += `BUSINESS RULES:
|
|
1359
|
+
`;
|
|
1360
|
+
contextData.rules.forEach((r) => memoryBackendContext += `- [${r.severity}] ${r.title}: ${r.description}
|
|
1361
|
+
`);
|
|
1362
|
+
}
|
|
1363
|
+
if (contextData.fields?.length > 0) {
|
|
1364
|
+
memoryBackendContext += `KNOWN FIELDS:
|
|
1365
|
+
`;
|
|
1366
|
+
contextData.fields.forEach((f) => memoryBackendContext += `- ${f.field_identifier} (${f.field_type}) - Required: ${f.is_required}
|
|
1367
|
+
`);
|
|
1368
|
+
}
|
|
1369
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Contexto de Memoria Colectiva inyectado (${contextData.rules?.length || 0} reglas, ${contextData.fields?.length || 0} campos).`);
|
|
1370
|
+
}
|
|
1371
|
+
} catch (e) {
|
|
1372
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de contexto: ${e.message}`);
|
|
1373
|
+
}
|
|
1374
|
+
const systemPromptBlocks = [
|
|
1375
|
+
{
|
|
1376
|
+
type: "text",
|
|
1377
|
+
text: `# IDENTITY
|
|
1378
|
+
You are **Arcality**, an elite Senior QA Web Testing Agent. You have 10+ years of experience in manual and automated testing. You are meticulous, thorough, and NEVER take shortcuts. You treat every mission as a real client engagement where your professional reputation is on the line. You MUST use your full arsenal of QA tools \u2014 guessing and blind retries are UNACCEPTABLE.
|
|
36
1379
|
|
|
37
1380
|
# MISSION
|
|
38
|
-
${
|
|
39
|
-
|
|
40
|
-
${
|
|
41
|
-
${
|
|
42
|
-
${
|
|
43
|
-
${
|
|
44
|
-
${
|
|
45
|
-
|
|
1381
|
+
${prompt}
|
|
1382
|
+
|
|
1383
|
+
${projectInfo}
|
|
1384
|
+
${skillsContext}
|
|
1385
|
+
${memoryContext}
|
|
1386
|
+
${credentialsContext}
|
|
1387
|
+
${memoryBackendContext}`
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
type: "text",
|
|
1391
|
+
text: `
|
|
1392
|
+
# METHODOLOGY (MANDATORY \u2014 Follow in order)
|
|
46
1393
|
Every turn, you MUST follow these 4 phases:
|
|
47
1394
|
|
|
48
1395
|
## Phase 1: OBSERVE
|
|
49
1396
|
- Read ALL components in CURRENT COMPONENTS carefully.
|
|
50
|
-
- Check the URL \
|
|
51
|
-
- Read HISTORY \
|
|
52
|
-
- Look for MESSAGE components \
|
|
53
|
-
- If you see [INFERRED] in any component name \
|
|
1397
|
+
- Check the URL \u2014 are you on the right page?
|
|
1398
|
+
- Read HISTORY \u2014 what have you already done? Don't repeat actions.
|
|
1399
|
+
- Look for MESSAGE components \u2014 errors ([ERROR_CR\xCDTICO]) or success messages.
|
|
1400
|
+
- If you see [INFERRED] in any component name \u2192 that name is UNRELIABLE. You MUST call \`inspect_element_details\` on it BEFORE interacting.
|
|
54
1401
|
|
|
55
1402
|
## Phase 2: PLAN
|
|
56
1403
|
- Based on your observations, decide: What is the ONE next meaningful step toward completing the mission?
|
|
@@ -61,8 +1408,8 @@ Every turn, you MUST follow these 4 phases:
|
|
|
61
1408
|
## Phase 3: ACT
|
|
62
1409
|
- Execute your planned actions using \`perform_ui_actions\`.
|
|
63
1410
|
- Group multiple related actions in one call (e.g., click+fill for 3 fields = 6 actions in one call).
|
|
64
|
-
- For dropdowns/selects: Click to open \
|
|
65
|
-
- For menus: Click menu icon \
|
|
1411
|
+
- For dropdowns/selects: Click to open \u2192 WAIT for next turn to see options \u2192 Select.
|
|
1412
|
+
- For menus: Click menu icon \u2192 WAIT for next turn \u2192 Click option.
|
|
66
1413
|
|
|
67
1414
|
## Phase 4: VERIFY
|
|
68
1415
|
- After critical actions (SAVE/CREATE/DELETE), verify the result:
|
|
@@ -85,7 +1432,7 @@ These are NOT optional. You MUST use these tools in these situations:
|
|
|
85
1432
|
| Bug found in application | \`create_test_evidence\` | To document with annotated screenshot |
|
|
86
1433
|
| Mission complete | \`create_test_evidence\` | To document the final successful state |
|
|
87
1434
|
| Modal/popup blocking page | \`send_keyboard_event\` (Escape) | To close the overlay |
|
|
88
|
-
| Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \
|
|
1435
|
+
| Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \u2014 native inputs need special handling |
|
|
89
1436
|
| Element not found on screen | \`scroll_page\` | To reveal off-screen content |
|
|
90
1437
|
| Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | After inspecting options |
|
|
91
1438
|
| Navigate between form sections | \`send_keyboard_event\` (Tab) | To move focus between fields |
|
|
@@ -97,7 +1444,7 @@ These are NOT optional. You MUST use these tools in these situations:
|
|
|
97
1444
|
3. **CURRENT STATE > MEMORY**: What you see in CURRENT COMPONENTS is the truth. SUCCESS MEMORY is a guide, not gospel.
|
|
98
1445
|
4. **PAGE TRANSITIONS**: If URL contains '/edit' or you see a 'Save' button, you ARE in the edit view. Don't go back.
|
|
99
1446
|
5. **MENUS & DROPDOWNS**: After clicking a toggle/menu, WAIT for the next turn. Never click the same menu icon twice in a row.
|
|
100
|
-
6. **ERROR HANDLING**: If you see '\
|
|
1447
|
+
6. **ERROR HANDLING**: If you see '\u{1F6D1} [ERROR_CR\xCDTICO]', STOP everything. Analyze the error, fix the data, then retry.
|
|
101
1448
|
7. **DATA UNIQUENESS**: When generating test data, ALWAYS include a unique suffix (timestamp, counter). Never use "Test123" or "Prueba" alone.
|
|
102
1449
|
8. **FINISH CORRECTLY**: Use \`finish: true\` ONLY when the mission objective is FULLY achieved.
|
|
103
1450
|
- **BEWARE**: If you see a record in a table, confirm it is the one you JUST created (e.g., check timestamp or log message). Do NOT assume an existing record is your success.
|
|
@@ -106,9 +1453,34 @@ These are NOT optional. You MUST use these tools in these situations:
|
|
|
106
1453
|
10. **STUCK DETECTION**: If history shows 3+ similar actions without progress, use \`report_inability_to_proceed\`.
|
|
107
1454
|
11. **NEGATIVE TESTING**: If mission expects an error (e.g., "verifica que no se pueda guardar sin campos"), and you see that error, the mission is SUCCESS. Use \`finish: true\`.
|
|
108
1455
|
12. **GUIDE CONFLICT**: If SUCCESS MEMORY disagrees with what you see on screen, ABANDON the memory and trust your eyes.
|
|
109
|
-
13. **NATIVE CONTROLS**: For \`<input type="time">\`, \`<input type="date">\`, or similar native pickers, NEVER use \`fill\` repeatedly. Use \`interact_native_control\` or \`send_keyboard_event\` instead. If \`fill\` fails once, switch strategy immediately.`,
|
|
110
|
-
|
|
111
|
-
|
|
1456
|
+
13. **NATIVE CONTROLS**: For \`<input type="time">\`, \`<input type="date">\`, or similar native pickers, NEVER use \`fill\` repeatedly. Use \`interact_native_control\` or \`send_keyboard_event\` instead. If \`fill\` fails once, switch strategy immediately.`,
|
|
1457
|
+
cache_control: { type: "ephemeral" }
|
|
1458
|
+
}
|
|
1459
|
+
];
|
|
1460
|
+
if (this.testInfo) {
|
|
1461
|
+
await this.testInfo.attach(`turn-${history.length}-vision`, {
|
|
1462
|
+
body: screenshot,
|
|
1463
|
+
contentType: "image/png"
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
const anthropicMessages = [
|
|
1467
|
+
{
|
|
1468
|
+
role: "user",
|
|
1469
|
+
content: [
|
|
1470
|
+
{
|
|
1471
|
+
type: "image",
|
|
1472
|
+
source: {
|
|
1473
|
+
type: "base64",
|
|
1474
|
+
media_type: "image/png",
|
|
1475
|
+
data: base64Img
|
|
1476
|
+
},
|
|
1477
|
+
cache_control: { type: "ephemeral" }
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
type: "text",
|
|
1481
|
+
text: `MISSION STATUS:
|
|
1482
|
+
- URL: ${state.url}
|
|
1483
|
+
- Title: ${state.title}
|
|
112
1484
|
|
|
113
1485
|
PROGRESS TRACKING:
|
|
114
1486
|
- IMPORTANT: This mission is a SEQUENCE of steps. Analyze the HISTORY to see which steps are DONE.
|
|
@@ -117,33 +1489,746 @@ PROGRESS TRACKING:
|
|
|
117
1489
|
- DO NOT restart or go back to previous steps if you are already in the target view.
|
|
118
1490
|
|
|
119
1491
|
CURRENT COMPONENTS:
|
|
120
|
-
${
|
|
1492
|
+
${componentsList}
|
|
121
1493
|
|
|
122
1494
|
CONSOLE LOGS:
|
|
123
|
-
${this.logs.join(
|
|
124
|
-
`)||"None"}
|
|
1495
|
+
${this.logs.join("\n") || "None"}
|
|
125
1496
|
|
|
126
1497
|
HISTORY (last 25 steps):
|
|
127
|
-
${
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
1498
|
+
${history.slice(-25).join("\n") || "None"}`
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
type: "text",
|
|
1502
|
+
text: `Please analyze the current state and provide the next step.`,
|
|
1503
|
+
cache_control: { type: "ephemeral" }
|
|
1504
|
+
}
|
|
1505
|
+
]
|
|
1506
|
+
}
|
|
1507
|
+
];
|
|
1508
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Arcality est\xE1 procesando la visi\xF3n...`);
|
|
1509
|
+
const startTime = Date.now();
|
|
1510
|
+
for (let turn = 0; turn < 10; turn++) {
|
|
1511
|
+
let response;
|
|
1512
|
+
let retryCount = 0;
|
|
1513
|
+
const maxRetries = 3;
|
|
1514
|
+
while (true) {
|
|
1515
|
+
const isProxyMode = !!process.env.ARCALITY_API_URL;
|
|
1516
|
+
const endpointUrl = isProxyMode ? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy` : "https://api.anthropic.com/v1/messages";
|
|
1517
|
+
const displayTurn = stepCount !== void 0 ? stepCount : turn + 1;
|
|
1518
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4E1} Llamando a: ${endpointUrl} (turno IA ${displayTurn}${turn > 0 ? `, reintento ${turn}` : ""})`);
|
|
1519
|
+
const headers = {
|
|
1520
|
+
"Content-Type": "application/json"
|
|
1521
|
+
};
|
|
1522
|
+
if (isProxyMode) {
|
|
1523
|
+
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
1524
|
+
const missionId = process.env.ARCALITY_MISSION_ID || "";
|
|
1525
|
+
if (missionId)
|
|
1526
|
+
headers["x-mission-id"] = missionId;
|
|
1527
|
+
} else {
|
|
1528
|
+
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
|
|
1529
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
1530
|
+
}
|
|
1531
|
+
const controller = new AbortController();
|
|
1532
|
+
const timeoutId = setTimeout(() => controller.abort(), 9e4);
|
|
1533
|
+
try {
|
|
1534
|
+
response = await fetch(endpointUrl, {
|
|
1535
|
+
method: "POST",
|
|
1536
|
+
headers,
|
|
1537
|
+
signal: controller.signal,
|
|
1538
|
+
body: JSON.stringify({
|
|
1539
|
+
model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
|
|
1540
|
+
max_tokens: 4096,
|
|
1541
|
+
system: systemPromptBlocks,
|
|
1542
|
+
tools: rawTools.map((t) => ({
|
|
1543
|
+
name: t.name,
|
|
1544
|
+
description: t.description,
|
|
1545
|
+
input_schema: t.parameters || t.input_schema
|
|
1546
|
+
})),
|
|
1547
|
+
messages: anthropicMessages,
|
|
1548
|
+
temperature: 0.2
|
|
1549
|
+
})
|
|
1550
|
+
});
|
|
1551
|
+
} catch (fetchErr) {
|
|
1552
|
+
clearTimeout(timeoutId);
|
|
1553
|
+
if (fetchErr.name === "AbortError") {
|
|
1554
|
+
throw new Error(`Arcality Brain Timeout: No response from ${endpointUrl} after 90s`);
|
|
1555
|
+
}
|
|
1556
|
+
throw new Error(`Arcality Brain Network Error: ${fetchErr.message}`);
|
|
1557
|
+
}
|
|
1558
|
+
clearTimeout(timeoutId);
|
|
1559
|
+
if (response.ok)
|
|
1560
|
+
break;
|
|
1561
|
+
const errorText = await response.text();
|
|
1562
|
+
console.log(`>>ARCALITY_STATUS>> \u274C API Error ${response.status}: ${errorText.substring(0, 500)}`);
|
|
1563
|
+
const isTransient = response.status === 529 || response.status === 429 || response.status === 503;
|
|
1564
|
+
if (isTransient && retryCount < maxRetries) {
|
|
1565
|
+
retryCount++;
|
|
1566
|
+
const delay = Math.pow(2, retryCount) * 1e3;
|
|
1567
|
+
console.log(`
|
|
1568
|
+
>>ARCALITY_STATUS>> \u26A0\uFE0F Arcality est\xE1 saturado (${response.status}). Reintentando...`);
|
|
1569
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
throw new Error(`Arcality Brain Error ${response.status}: ${errorText}`);
|
|
1573
|
+
}
|
|
1574
|
+
const data = await response.json();
|
|
1575
|
+
const message = data;
|
|
1576
|
+
anthropicMessages.push({
|
|
1577
|
+
role: "assistant",
|
|
1578
|
+
content: message.content
|
|
1579
|
+
});
|
|
1580
|
+
const toolCalls = message.content.filter((c) => c.type === "tool_use");
|
|
1581
|
+
const textResponse = message.content.find((c) => c.type === "text")?.text || "";
|
|
1582
|
+
if (toolCalls.length === 0) {
|
|
1583
|
+
console.log(`(${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
|
|
1584
|
+
return { thought: textResponse || "No action decided", actions: [] };
|
|
1585
|
+
}
|
|
1586
|
+
const toolResults = [];
|
|
1587
|
+
let uiActionCall = null;
|
|
1588
|
+
for (const toolUse of toolCalls) {
|
|
1589
|
+
const toolName = toolUse.name;
|
|
1590
|
+
const toolInput = toolUse.input;
|
|
1591
|
+
if (toolName === "perform_ui_actions") {
|
|
1592
|
+
uiActionCall = { id: toolUse.id, input: toolInput };
|
|
1593
|
+
if (toolCalls.length > 1) {
|
|
1594
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "UI Action buffered." });
|
|
1595
|
+
}
|
|
1596
|
+
} else if (toolName === "inspect_element_details") {
|
|
1597
|
+
const idx = toolInput.idx;
|
|
1598
|
+
const c = state.components.find((comp) => comp.idx === idx);
|
|
1599
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F50D} Investigando IDX:${idx}...`);
|
|
1600
|
+
let details = "Element not found or no longer present.";
|
|
1601
|
+
if (c) {
|
|
1602
|
+
try {
|
|
1603
|
+
details = await this.page.frames()[c.frameIdx].evaluate((s) => {
|
|
1604
|
+
const el = document.querySelector(s);
|
|
1605
|
+
if (!el)
|
|
1606
|
+
return "Not found";
|
|
1607
|
+
const styles = window.getComputedStyle(el);
|
|
1608
|
+
return JSON.stringify({
|
|
1609
|
+
tag: el.tagName,
|
|
1610
|
+
id: el.id,
|
|
1611
|
+
classes: el.className,
|
|
1612
|
+
isVisible: el.offsetParent !== null,
|
|
1613
|
+
rect: el.getBoundingClientRect(),
|
|
1614
|
+
attributes: Array.from(el.attributes).map((a) => `${a.name}="${a.value}"`),
|
|
1615
|
+
style: { display: styles.display, pointerEvents: styles.pointerEvents, opacity: styles.opacity }
|
|
1616
|
+
}, null, 2);
|
|
1617
|
+
}, c.selector);
|
|
1618
|
+
} catch (e) {
|
|
1619
|
+
details = `Error inspecting: ${e.message}`;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: details });
|
|
1623
|
+
} else if (toolName === "report_application_bug") {
|
|
1624
|
+
console.error(`\u{1F41B} [BUG DETECTED] ${toolInput.bug_description}`);
|
|
1625
|
+
try {
|
|
1626
|
+
const url = this.page.url();
|
|
1627
|
+
await this.knowledgeService.saveRule(
|
|
1628
|
+
"Validaci\xF3n Detectada (Auto-Learning)",
|
|
1629
|
+
`Error: ${toolInput.bug_description}. Se esperaba: ${toolInput.expected_behavior}`,
|
|
1630
|
+
"WARNING",
|
|
1631
|
+
url
|
|
1632
|
+
);
|
|
1633
|
+
} catch (e) {
|
|
1634
|
+
}
|
|
1635
|
+
throw new Error(`APPLICATION BUG: ${toolInput.bug_description}`);
|
|
1636
|
+
} else if (toolName === "report_inability_to_proceed") {
|
|
1637
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Confusion acknowledged." });
|
|
1638
|
+
return {
|
|
1639
|
+
thought: `DISCULPA: ${toolInput.confusion_reason}
|
|
1640
|
+
|
|
1641
|
+
\u{1F4A1} SUGERENCIA: ${toolInput.prompt_suggestion}`,
|
|
1642
|
+
actions: [],
|
|
1643
|
+
finish: true
|
|
1644
|
+
};
|
|
1645
|
+
} else if (toolName === "validate_element_state") {
|
|
1646
|
+
const { idx, validations } = toolInput;
|
|
1647
|
+
const c = state.components.find((comp) => comp.idx === idx);
|
|
1648
|
+
let result = "";
|
|
1649
|
+
if (!c) {
|
|
1650
|
+
result = "Element not found.";
|
|
1651
|
+
} else {
|
|
1652
|
+
try {
|
|
1653
|
+
result = await this.page.frames()[c.frameIdx].evaluate(({ selector, validations: validations2 }) => {
|
|
1654
|
+
const el = document.querySelector(selector);
|
|
1655
|
+
if (!el)
|
|
1656
|
+
return "Element no longer exists in DOM.";
|
|
1657
|
+
const styles = window.getComputedStyle(el);
|
|
1658
|
+
const results = validations2.map((v) => {
|
|
1659
|
+
let pass = false;
|
|
1660
|
+
let actual = "";
|
|
1661
|
+
switch (v.type) {
|
|
1662
|
+
case "exists":
|
|
1663
|
+
pass = !!el;
|
|
1664
|
+
actual = pass ? "exists" : "none";
|
|
1665
|
+
break;
|
|
1666
|
+
case "visible":
|
|
1667
|
+
pass = el.offsetParent !== null && styles.visibility !== "hidden" && styles.display !== "none" && parseFloat(styles.opacity) > 0;
|
|
1668
|
+
actual = pass ? "visible" : "hidden";
|
|
1669
|
+
break;
|
|
1670
|
+
case "enabled":
|
|
1671
|
+
pass = !el.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
1672
|
+
actual = pass ? "enabled" : "disabled";
|
|
1673
|
+
break;
|
|
1674
|
+
case "disabled":
|
|
1675
|
+
pass = el.disabled || el.getAttribute("aria-disabled") === "true";
|
|
1676
|
+
actual = pass ? "disabled" : "enabled";
|
|
1677
|
+
break;
|
|
1678
|
+
case "contains_text":
|
|
1679
|
+
actual = el.innerText || "";
|
|
1680
|
+
pass = actual.toLowerCase().includes(v.expected_value.toLowerCase());
|
|
1681
|
+
break;
|
|
1682
|
+
case "has_value":
|
|
1683
|
+
actual = el.value || "";
|
|
1684
|
+
pass = actual === v.expected_value;
|
|
1685
|
+
break;
|
|
1686
|
+
case "is_empty":
|
|
1687
|
+
actual = el.value || el.innerText || "";
|
|
1688
|
+
pass = actual.trim() === "";
|
|
1689
|
+
break;
|
|
1690
|
+
case "has_class":
|
|
1691
|
+
actual = el.className;
|
|
1692
|
+
pass = el.classList.contains(v.expected_value);
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
return `${v.type}: ${pass ? "PASS" : "FAIL"} (Actual: "${actual}")`;
|
|
1696
|
+
});
|
|
1697
|
+
return results.join("\n");
|
|
1698
|
+
}, { selector: c.selector, validations });
|
|
1699
|
+
} catch (e) {
|
|
1700
|
+
result = `Error validating: ${e.message}`;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
|
|
1704
|
+
} else if (toolName === "extract_table_data") {
|
|
1705
|
+
const { table_identifier, max_rows = 50 } = toolInput;
|
|
1706
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Extrayendo tabla: ${table_identifier}...`);
|
|
1707
|
+
try {
|
|
1708
|
+
const tableData = await this.page.evaluate(({ identifier, limit }) => {
|
|
1709
|
+
const findTable = () => {
|
|
1710
|
+
const headers = Array.from(document.querySelectorAll("h1, h2, h3, h4, th, label, span, p, div")).filter((el) => el.textContent?.includes(identifier));
|
|
1711
|
+
for (const h of headers) {
|
|
1712
|
+
let curr = h;
|
|
1713
|
+
for (let i = 0; i < 5; i++) {
|
|
1714
|
+
if (!curr)
|
|
1715
|
+
break;
|
|
1716
|
+
const table2 = curr.querySelector("table") || (curr.tagName === "TABLE" ? curr : null);
|
|
1717
|
+
if (table2)
|
|
1718
|
+
return table2;
|
|
1719
|
+
curr = curr.parentElement;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return document.querySelector("table");
|
|
1723
|
+
};
|
|
1724
|
+
const table = findTable();
|
|
1725
|
+
if (!table)
|
|
1726
|
+
return "No table found with identifier: " + identifier;
|
|
1727
|
+
const rows = Array.from(table.querySelectorAll("tr")).slice(0, limit);
|
|
1728
|
+
return rows.map((r) => {
|
|
1729
|
+
return Array.from(r.querySelectorAll("td, th")).map((c) => c.textContent?.trim() || "");
|
|
1730
|
+
});
|
|
1731
|
+
}, { identifier: table_identifier, limit: max_rows });
|
|
1732
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: JSON.stringify(tableData, null, 2) });
|
|
1733
|
+
} catch (e) {
|
|
1734
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error extracting: ${e.message}` });
|
|
1735
|
+
}
|
|
1736
|
+
} else if (toolName === "capture_console_errors") {
|
|
1737
|
+
const logs = this.logs.length > 0 ? this.logs.join("\n") : "No console errors/warnings/network errors captured.";
|
|
1738
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: logs });
|
|
1739
|
+
} else if (toolName === "assert_url_pattern") {
|
|
1740
|
+
const { pattern, should_match = true } = toolInput;
|
|
1741
|
+
const url = this.page.url();
|
|
1742
|
+
const match = url.includes(pattern);
|
|
1743
|
+
const pass = should_match ? match : !match;
|
|
1744
|
+
const result = `URL Assertion: ${pass ? "PASS" : "FAIL"} (Current URL: ${url} | Pattern: ${pattern} | Expected match: ${should_match})`;
|
|
1745
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
|
|
1746
|
+
} else if (toolName === "measure_page_performance") {
|
|
1747
|
+
const { thresholds = {} } = toolInput;
|
|
1748
|
+
console.log(`>>ARCALITY_STATUS>> \u26A1 Midiendo rendimiento de p\xE1gina...`);
|
|
1749
|
+
try {
|
|
1750
|
+
const perfData = await this.page.evaluate(() => {
|
|
1751
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
1752
|
+
const paint = performance.getEntriesByType("paint");
|
|
1753
|
+
const resources = performance.getEntriesByType("resource");
|
|
1754
|
+
const fcp = paint.find((p) => p.name === "first-contentful-paint");
|
|
1755
|
+
return {
|
|
1756
|
+
load_time_ms: nav ? Math.round(nav.loadEventEnd - nav.startTime) : null,
|
|
1757
|
+
dom_content_loaded_ms: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
|
1758
|
+
first_contentful_paint_ms: fcp ? Math.round(fcp.startTime) : null,
|
|
1759
|
+
network_requests: resources.length,
|
|
1760
|
+
total_resource_size_kb: Math.round(resources.reduce((sum, r) => sum + (r.transferSize || 0), 0) / 1024)
|
|
1761
|
+
};
|
|
1762
|
+
});
|
|
1763
|
+
const results = [
|
|
1764
|
+
`\u{1F4CA} PERFORMANCE METRICS:`,
|
|
1765
|
+
` Load Time: ${perfData.load_time_ms ?? "N/A"}ms`,
|
|
1766
|
+
` DOM Content Loaded: ${perfData.dom_content_loaded_ms ?? "N/A"}ms`,
|
|
1767
|
+
` First Contentful Paint: ${perfData.first_contentful_paint_ms ?? "N/A"}ms`,
|
|
1768
|
+
` Network Requests: ${perfData.network_requests}`,
|
|
1769
|
+
` Total Resource Size: ${perfData.total_resource_size_kb}KB`
|
|
1770
|
+
];
|
|
1771
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: results.join("\n") });
|
|
1772
|
+
} catch (e) {
|
|
1773
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error measuring performance: ${e.message}` });
|
|
1774
|
+
}
|
|
1775
|
+
} else if (toolName === "save_navigation_checkpoint") {
|
|
1776
|
+
const { checkpoint_name, include_storage = true } = toolInput;
|
|
1777
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4BE} Guardando punto de control: "${checkpoint_name}"...`);
|
|
1778
|
+
try {
|
|
1779
|
+
const url = this.page.url();
|
|
1780
|
+
const context = this.page.context();
|
|
1781
|
+
const storageState = await context.storageState();
|
|
1782
|
+
let localStorage = {};
|
|
1783
|
+
let sessionStorage = {};
|
|
1784
|
+
if (include_storage) {
|
|
1785
|
+
const storageData = await this.page.evaluate(() => {
|
|
1786
|
+
const ls = {};
|
|
1787
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
1788
|
+
const key = window.localStorage.key(i);
|
|
1789
|
+
if (key)
|
|
1790
|
+
ls[key] = window.localStorage.getItem(key) || "";
|
|
1791
|
+
}
|
|
1792
|
+
const ss = {};
|
|
1793
|
+
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
1794
|
+
const key = window.sessionStorage.key(i);
|
|
1795
|
+
if (key)
|
|
1796
|
+
ss[key] = window.sessionStorage.getItem(key) || "";
|
|
1797
|
+
}
|
|
1798
|
+
return { localStorage: ls, sessionStorage: ss };
|
|
1799
|
+
});
|
|
1800
|
+
localStorage = storageData.localStorage;
|
|
1801
|
+
sessionStorage = storageData.sessionStorage;
|
|
1802
|
+
}
|
|
1803
|
+
this.checkpoints.set(checkpoint_name, { url, storageState, localStorage, sessionStorage });
|
|
1804
|
+
const result = `\u2705 Checkpoint "${checkpoint_name}" saved successfully.`;
|
|
1805
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
|
|
1806
|
+
} catch (e) {
|
|
1807
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error saving checkpoint: ${e.message}` });
|
|
1808
|
+
}
|
|
1809
|
+
} else if (toolName === "restore_navigation_checkpoint") {
|
|
1810
|
+
const { checkpoint_name } = toolInput;
|
|
1811
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F504} Restaurando punto de control: "${checkpoint_name}"...`);
|
|
1812
|
+
const checkpoint = this.checkpoints.get(checkpoint_name);
|
|
1813
|
+
if (!checkpoint) {
|
|
1814
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u274C Checkpoint "${checkpoint_name}" not found.` });
|
|
1815
|
+
} else {
|
|
1816
|
+
try {
|
|
1817
|
+
const context = this.page.context();
|
|
1818
|
+
await context.clearCookies();
|
|
1819
|
+
if (checkpoint.storageState.cookies?.length) {
|
|
1820
|
+
await context.addCookies(checkpoint.storageState.cookies);
|
|
1821
|
+
}
|
|
1822
|
+
await this.page.goto(checkpoint.url, { waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
1823
|
+
await this.page.evaluate(({ ls, ss }) => {
|
|
1824
|
+
window.localStorage.clear();
|
|
1825
|
+
for (const [k, v] of Object.entries(ls))
|
|
1826
|
+
window.localStorage.setItem(k, v);
|
|
1827
|
+
window.sessionStorage.clear();
|
|
1828
|
+
for (const [k, v] of Object.entries(ss))
|
|
1829
|
+
window.sessionStorage.setItem(k, v);
|
|
1830
|
+
}, { ls: checkpoint.localStorage, ss: checkpoint.sessionStorage });
|
|
1831
|
+
await this.page.reload({ waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
1832
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Checkpoint "${checkpoint_name}" restored.` });
|
|
1833
|
+
} catch (e) {
|
|
1834
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error restoring checkpoint: ${e.message}` });
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
} else if (toolName === "test_xss_injection") {
|
|
1838
|
+
const { idx, payload_type } = toolInput;
|
|
1839
|
+
const el = state.components.find((c) => c.idx === idx);
|
|
1840
|
+
if (!el) {
|
|
1841
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Element idx ${idx} not found.` });
|
|
1842
|
+
} else {
|
|
1843
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Probando inyecci\xF3n XSS en IDX ${idx}...`);
|
|
1844
|
+
let payload = "<script>alert(1)</script>";
|
|
1845
|
+
if (payload_type === "image")
|
|
1846
|
+
payload = "<img src=x onerror=alert(1)>";
|
|
1847
|
+
if (payload_type === "svg")
|
|
1848
|
+
payload = "<svg onload=alert(1)>";
|
|
1849
|
+
try {
|
|
1850
|
+
const locator = this.page.locator(`xpath=${el.selector}`).first();
|
|
1851
|
+
await locator.fill(payload);
|
|
1852
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Payload inyectado. Aseg\xFArate de probar si el payload fue ejecutado revisando el DOM, o realiza el sumbit y observa si el input regres\xF3 renderizado tal cual o sanitizado.` });
|
|
1853
|
+
if (this.testInfo)
|
|
1854
|
+
this.testInfo.attach("security_tool_call", { body: JSON.stringify({ action: "test_xss_injection", target: idx, payload }), contentType: "application/json" });
|
|
1855
|
+
} catch (e) {
|
|
1856
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error injecting payload: ${e.message}` });
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
} else if (toolName === "test_auth_bypass") {
|
|
1860
|
+
const { target_url } = toolInput;
|
|
1861
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Intentando Auth Bypass en ${target_url}...`);
|
|
1862
|
+
try {
|
|
1863
|
+
const baseUrl = new URL(this.page.url()).origin;
|
|
1864
|
+
const fullUrl = new URL(target_url, baseUrl).toString();
|
|
1865
|
+
await this.page.goto(fullUrl);
|
|
1866
|
+
await this.page.waitForLoadState("networkidle", { timeout: 3e3 }).catch(() => {
|
|
1867
|
+
});
|
|
1868
|
+
const resultingUrl = this.page.url();
|
|
1869
|
+
let resultData = `Navegado a ${fullUrl}.
|
|
1870
|
+
URL resultante: ${resultingUrl}.
|
|
1871
|
+
`;
|
|
1872
|
+
if (resultingUrl !== fullUrl)
|
|
1873
|
+
resultData += `Posiblemente redirigido (el bypass fall\xF3 o te enviaron a Login).`;
|
|
1874
|
+
else
|
|
1875
|
+
resultData += `URL se mantuvo igual. \xA1Esto podr\xEDa ser un IDOR o Auth Bypass si ves datos a los que no deber\xEDas acceder!`;
|
|
1876
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: resultData });
|
|
1877
|
+
} catch (e) {
|
|
1878
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error test_auth_bypass: ${e.message}` });
|
|
1879
|
+
}
|
|
1880
|
+
} else if (toolName === "scan_sensitive_data_exposure") {
|
|
1881
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Escaneando datos sensibles...`);
|
|
1882
|
+
try {
|
|
1883
|
+
const issues = await scan_sensitive_data_exposure(this.page);
|
|
1884
|
+
if (issues.length > 0) {
|
|
1885
|
+
const report = issues.map((i) => `- ${i.category}: ${i.description}
|
|
1886
|
+
Evidence: ${JSON.stringify(i.evidence)}`).join("\n");
|
|
1887
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Vulnerabilidades detectadas:
|
|
1888
|
+
${report}` });
|
|
1889
|
+
} else {
|
|
1890
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron datos sensibles en LocalStorage/SessionStorage en este chequeo." });
|
|
1891
|
+
}
|
|
1892
|
+
} catch (e) {
|
|
1893
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scan_sensitive_data_exposure: ${e.message}` });
|
|
1894
|
+
}
|
|
1895
|
+
} else if (toolName === "audit_http_headers") {
|
|
1896
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Auditando headers HTTP...`);
|
|
1897
|
+
try {
|
|
1898
|
+
const issues = await audit_http_headers(this.page);
|
|
1899
|
+
if (issues.length > 0) {
|
|
1900
|
+
const report = issues.map((i) => `- ${i.category}: ${i.description}`).join("\n");
|
|
1901
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Vulnerabilidades de Headers detectadas:
|
|
1902
|
+
${report}` });
|
|
1903
|
+
} else {
|
|
1904
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron problemas en los headers de seguridad b\xE1sicos." });
|
|
1905
|
+
}
|
|
1906
|
+
} catch (e) {
|
|
1907
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error audit_http_headers: ${e.message}` });
|
|
1908
|
+
}
|
|
1909
|
+
} else if (toolName === "create_test_evidence") {
|
|
1910
|
+
const { evidence_type, annotations, save_as, description, finish_run = false } = toolInput;
|
|
1911
|
+
console.log(`\u{1F4F8} [SKILL] Creating test evidence: "${save_as}"...`);
|
|
1912
|
+
try {
|
|
1913
|
+
const filename = save_as.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1914
|
+
if (annotations && annotations.length > 0) {
|
|
1915
|
+
await this.page.evaluate((anns) => {
|
|
1916
|
+
anns.forEach((ann) => {
|
|
1917
|
+
const el = document.querySelector(`[agent-idx="${ann.idx}"]`);
|
|
1918
|
+
if (el) {
|
|
1919
|
+
const rect = el.getBoundingClientRect();
|
|
1920
|
+
const div = document.createElement("div");
|
|
1921
|
+
div.className = "arcality-annotation";
|
|
1922
|
+
div.style.position = "absolute";
|
|
1923
|
+
div.style.border = `3px solid ${ann.color || "red"}`;
|
|
1924
|
+
div.style.top = `${rect.top + window.scrollY}px`;
|
|
1925
|
+
div.style.left = `${rect.left + window.scrollX}px`;
|
|
1926
|
+
div.style.width = `${rect.width}px`;
|
|
1927
|
+
div.style.height = `${rect.height}px`;
|
|
1928
|
+
div.style.pointerEvents = "none";
|
|
1929
|
+
div.style.zIndex = "1000000";
|
|
1930
|
+
const label = document.createElement("span");
|
|
1931
|
+
label.innerText = ann.label;
|
|
1932
|
+
label.style.background = ann.color || "red";
|
|
1933
|
+
label.style.color = "white";
|
|
1934
|
+
label.style.fontSize = "12px";
|
|
1935
|
+
label.style.padding = "2px 4px";
|
|
1936
|
+
label.style.position = "absolute";
|
|
1937
|
+
label.style.top = "-20px";
|
|
1938
|
+
label.style.left = "0";
|
|
1939
|
+
div.appendChild(label);
|
|
1940
|
+
document.body.appendChild(div);
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
}, annotations);
|
|
1944
|
+
}
|
|
1945
|
+
let buffer = await this.page.screenshot({
|
|
1946
|
+
type: "png",
|
|
1947
|
+
fullPage: evidence_type === "full_page_screenshot"
|
|
1948
|
+
});
|
|
1949
|
+
await this.page.evaluate(() => {
|
|
1950
|
+
document.querySelectorAll(".arcality-annotation").forEach((el) => el.remove());
|
|
1951
|
+
});
|
|
1952
|
+
if (this.testInfo) {
|
|
1953
|
+
await this.testInfo.attach(filename, { body: buffer, contentType: "image/png" });
|
|
1954
|
+
if (description) {
|
|
1955
|
+
await this.testInfo.attach(`${filename}_description`, {
|
|
1956
|
+
body: Buffer.from(description, "utf-8"),
|
|
1957
|
+
contentType: "text/plain"
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Evidence "${filename}" created and attached.` });
|
|
1961
|
+
if (finish_run) {
|
|
1962
|
+
this.lastScreenshot = base64Img;
|
|
1963
|
+
return { thought: `Evidence "${description || filename}" created. Mission complete.`, actions: [], finish: true };
|
|
1964
|
+
}
|
|
1965
|
+
} else {
|
|
1966
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u26A0\uFE0F testInfo not available.` });
|
|
1967
|
+
}
|
|
1968
|
+
} catch (e) {
|
|
1969
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error creating evidence: ${e.message}` });
|
|
1970
|
+
}
|
|
1971
|
+
} else if (toolName === "send_keyboard_event") {
|
|
1972
|
+
const { key, idx, repeat = 1 } = toolInput;
|
|
1973
|
+
console.log(`>>ARCALITY_STATUS>> \u2328\uFE0F Enviando tecla: "${key}" ${idx !== void 0 ? `(focus en IDX:${idx})` : "(focus actual)"} x${repeat}`);
|
|
1974
|
+
try {
|
|
1975
|
+
if (idx !== void 0) {
|
|
1976
|
+
const c = state.components.find((comp) => comp.idx === idx);
|
|
1977
|
+
if (c) {
|
|
1978
|
+
const loc = this.page.frames()[c.frameIdx].locator(c.selector).first();
|
|
1979
|
+
await loc.focus({ timeout: 5e3 }).catch(async () => {
|
|
1980
|
+
await loc.click({ timeout: 5e3, force: true });
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
for (let i = 0; i < Math.min(repeat, 20); i++) {
|
|
1985
|
+
await this.page.keyboard.press(key);
|
|
1986
|
+
if (repeat > 1)
|
|
1987
|
+
await this.page.waitForTimeout(100);
|
|
1988
|
+
}
|
|
1989
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Key "${key}" pressed ${repeat} time(s).` });
|
|
1990
|
+
} catch (e) {
|
|
1991
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error sending key: ${e.message}` });
|
|
1992
|
+
}
|
|
1993
|
+
} else if (toolName === "interact_native_control") {
|
|
1994
|
+
const { idx, control_type, value } = toolInput;
|
|
1995
|
+
const c = state.components.find((comp) => comp.idx === idx);
|
|
1996
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F39B}\uFE0F Control nativo: ${control_type} IDX:${idx} \u2192 "${value}"`);
|
|
1997
|
+
if (!c) {
|
|
1998
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Element not found." });
|
|
1999
|
+
} else {
|
|
2000
|
+
try {
|
|
2001
|
+
const frame = this.page.frames()[c.frameIdx];
|
|
2002
|
+
const loc = frame.locator(c.selector).first();
|
|
2003
|
+
switch (control_type) {
|
|
2004
|
+
case "time":
|
|
2005
|
+
case "date":
|
|
2006
|
+
case "datetime": {
|
|
2007
|
+
try {
|
|
2008
|
+
await loc.fill(value, { timeout: 3e3 });
|
|
2009
|
+
const actualValue = await loc.inputValue();
|
|
2010
|
+
if (actualValue && actualValue !== "") {
|
|
2011
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 ${control_type} set to "${actualValue}" via fill().` });
|
|
2012
|
+
break;
|
|
2013
|
+
}
|
|
2014
|
+
} catch {
|
|
2015
|
+
}
|
|
2016
|
+
await loc.click({ force: true });
|
|
2017
|
+
await this.page.keyboard.press("Control+a");
|
|
2018
|
+
await this.page.waitForTimeout(100);
|
|
2019
|
+
const digits = value.replace(/[:-T]/g, "");
|
|
2020
|
+
await this.page.keyboard.type(digits, { delay: 50 });
|
|
2021
|
+
const finalValue = await loc.inputValue().catch(() => "");
|
|
2022
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 ${control_type} set via keyboard. Current value: "${finalValue}".` });
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
case "color":
|
|
2026
|
+
await loc.fill(value, { timeout: 3e3 });
|
|
2027
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Color set to "${value}".` });
|
|
2028
|
+
break;
|
|
2029
|
+
case "range": {
|
|
2030
|
+
const numVal = parseFloat(value);
|
|
2031
|
+
await loc.fill(String(numVal), { timeout: 3e3 }).catch(async () => {
|
|
2032
|
+
await frame.evaluate(({ sel, val }) => {
|
|
2033
|
+
const el = document.querySelector(sel);
|
|
2034
|
+
if (el) {
|
|
2035
|
+
el.value = val;
|
|
2036
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2037
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2038
|
+
}
|
|
2039
|
+
}, { sel: c.selector, val: String(numVal) });
|
|
2040
|
+
});
|
|
2041
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Range set to ${numVal}.` });
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
case "select": {
|
|
2045
|
+
try {
|
|
2046
|
+
await loc.selectOption({ label: value }, { timeout: 3e3 });
|
|
2047
|
+
} catch {
|
|
2048
|
+
try {
|
|
2049
|
+
await loc.selectOption({ value }, { timeout: 3e3 });
|
|
2050
|
+
} catch {
|
|
2051
|
+
await loc.selectOption(value, { timeout: 3e3 });
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const selected = await loc.inputValue().catch(() => "unknown");
|
|
2055
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Option selected. Value: "${selected}".` });
|
|
2056
|
+
break;
|
|
2057
|
+
}
|
|
2058
|
+
case "contenteditable":
|
|
2059
|
+
await loc.click({ force: true });
|
|
2060
|
+
await this.page.keyboard.press("Control+a");
|
|
2061
|
+
await this.page.keyboard.type(value, { delay: 30 });
|
|
2062
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Content set to "${value}".` });
|
|
2063
|
+
break;
|
|
2064
|
+
default:
|
|
2065
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u274C Unknown control type: ${control_type}` });
|
|
2066
|
+
}
|
|
2067
|
+
} catch (e) {
|
|
2068
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error interacting with native control: ${e.message}` });
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
} else if (toolName === "scroll_page") {
|
|
2072
|
+
const { direction, amount = 500, idx } = toolInput;
|
|
2073
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4DC} Scroll: ${direction} ${amount}px ${idx !== void 0 ? `(container IDX:${idx})` : "(page)"}`);
|
|
2074
|
+
try {
|
|
2075
|
+
if (idx !== void 0) {
|
|
2076
|
+
const c = state.components.find((comp) => comp.idx === idx);
|
|
2077
|
+
if (c) {
|
|
2078
|
+
const loc = this.page.frames()[c.frameIdx].locator(c.selector).first();
|
|
2079
|
+
await loc.evaluate((el, args) => {
|
|
2080
|
+
const d = args.direction;
|
|
2081
|
+
const a = args.amount;
|
|
2082
|
+
if (d === "top")
|
|
2083
|
+
el.scrollTop = 0;
|
|
2084
|
+
else if (d === "bottom")
|
|
2085
|
+
el.scrollTop = el.scrollHeight;
|
|
2086
|
+
else if (d === "down")
|
|
2087
|
+
el.scrollTop += a;
|
|
2088
|
+
else if (d === "up")
|
|
2089
|
+
el.scrollTop -= a;
|
|
2090
|
+
else if (d === "left")
|
|
2091
|
+
el.scrollLeft -= a;
|
|
2092
|
+
else if (d === "right")
|
|
2093
|
+
el.scrollLeft += a;
|
|
2094
|
+
}, { direction, amount });
|
|
2095
|
+
}
|
|
2096
|
+
} else {
|
|
2097
|
+
switch (direction) {
|
|
2098
|
+
case "down":
|
|
2099
|
+
await this.page.mouse.wheel(0, amount);
|
|
2100
|
+
break;
|
|
2101
|
+
case "up":
|
|
2102
|
+
await this.page.mouse.wheel(0, -amount);
|
|
2103
|
+
break;
|
|
2104
|
+
case "right":
|
|
2105
|
+
await this.page.mouse.wheel(amount, 0);
|
|
2106
|
+
break;
|
|
2107
|
+
case "left":
|
|
2108
|
+
await this.page.mouse.wheel(-amount, 0);
|
|
2109
|
+
break;
|
|
2110
|
+
case "top":
|
|
2111
|
+
await this.page.evaluate(() => window.scrollTo(0, 0));
|
|
2112
|
+
break;
|
|
2113
|
+
case "bottom":
|
|
2114
|
+
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
2115
|
+
break;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
await this.page.waitForTimeout(300);
|
|
2119
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `\u2705 Scrolled ${direction} ${direction === "top" || direction === "bottom" ? "completely" : `${amount}px`}. Page state will refresh on next turn.` });
|
|
2120
|
+
} catch (e) {
|
|
2121
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scrolling: ${e.message}` });
|
|
2122
|
+
}
|
|
2123
|
+
} else {
|
|
2124
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error: Tool '${toolName}' not recognized by this version.` });
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (toolResults.length > 0) {
|
|
2128
|
+
anthropicMessages.push({
|
|
2129
|
+
role: "user",
|
|
2130
|
+
content: toolResults
|
|
2131
|
+
});
|
|
2132
|
+
if (!uiActionCall)
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
if (uiActionCall) {
|
|
2136
|
+
const input = uiActionCall.input;
|
|
2137
|
+
console.log(`(${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
|
|
2138
|
+
this.lastScreenshot = base64Img;
|
|
2139
|
+
return {
|
|
2140
|
+
thought: input.thought,
|
|
2141
|
+
actions: input.actions.map((a) => {
|
|
2142
|
+
const c = state.components.find((comp) => comp.idx === a.idx);
|
|
2143
|
+
let finalValue = a.value;
|
|
2144
|
+
if (a.action === "fill" && finalValue && /^\d{2}\/\d{2}\/\d{4}$/.test(finalValue)) {
|
|
2145
|
+
const [d, m, y] = finalValue.split("/");
|
|
2146
|
+
finalValue = `${y}-${m}-${d}`;
|
|
2147
|
+
}
|
|
2148
|
+
return { ...a, value: finalValue, selector: c?.selector, frameIdx: c?.frameIdx, description: c?.name, type: c?.type };
|
|
2149
|
+
}),
|
|
2150
|
+
finish: input.finish
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return { thought: "Max internal turns reached", actions: [] };
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* QA Skill: Genera una explicación "humana" y amigable de por qué la misión fue exitosa.
|
|
2158
|
+
*/
|
|
2159
|
+
async humanizeMissionResult(prompt, history) {
|
|
2160
|
+
if (!process.env.ANTHROPIC_API_KEY)
|
|
2161
|
+
return null;
|
|
2162
|
+
try {
|
|
2163
|
+
const isProxyMode = !!process.env.ARCALITY_API_URL;
|
|
2164
|
+
const endpointUrl = isProxyMode ? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy` : "https://api.anthropic.com/v1/messages";
|
|
2165
|
+
const headers = {
|
|
2166
|
+
"Content-Type": "application/json"
|
|
2167
|
+
};
|
|
2168
|
+
if (isProxyMode) {
|
|
2169
|
+
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
2170
|
+
const missionId = process.env.ARCALITY_MISSION_ID || "";
|
|
2171
|
+
if (missionId)
|
|
2172
|
+
headers["x-mission-id"] = missionId;
|
|
2173
|
+
} else {
|
|
2174
|
+
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
|
|
2175
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
2176
|
+
}
|
|
2177
|
+
const response = await fetch(endpointUrl, {
|
|
2178
|
+
method: "POST",
|
|
2179
|
+
headers,
|
|
2180
|
+
body: JSON.stringify({
|
|
2181
|
+
model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
|
|
2182
|
+
max_tokens: 300,
|
|
2183
|
+
system: "Eres un Senior QA personalizador de reportes. Tu tarea es resumir en una sola frase corta, profesional y amigable por qu\xE9 una misi\xF3n de testing fue exitosa bas\xE1ndote en el historial de pasos. Usa un tono de victoria. No menciones detalles t\xE9cnicos como 'selectores' o 'DOM'. El resumen debe empezar con algo como '\xA1Misi\xF3n cumplida! ...'",
|
|
2184
|
+
messages: [
|
|
2185
|
+
{ role: "user", content: `Misi\xF3n original: ${prompt}
|
|
142
2186
|
|
|
143
2187
|
history of steps:
|
|
144
|
-
${
|
|
145
|
-
|
|
146
|
-
|
|
2188
|
+
${history.join("\n")}` }
|
|
2189
|
+
]
|
|
2190
|
+
})
|
|
2191
|
+
});
|
|
2192
|
+
const data = await response.json();
|
|
2193
|
+
return data.content?.[0]?.text || null;
|
|
2194
|
+
} catch (e) {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* MODO BRIDGE: Envía la percepción de la CLI hacia el Portal Web (SaaS)
|
|
2200
|
+
* para que el servidor decida la acción usando su propia infraestructura e IP.
|
|
2201
|
+
// askBridge eliminado — proxy mode ahora está integrado directamente en askIA.
|
|
2202
|
+
|
|
2203
|
+
/**
|
|
2204
|
+
* QA Skill: Summarizes the mission execution into a clean, human-friendly English YAML format.
|
|
2205
|
+
*/
|
|
2206
|
+
async summarizeMissionToYaml(prompt, history, startUrl) {
|
|
2207
|
+
if (!process.env.ANTHROPIC_API_KEY)
|
|
2208
|
+
return null;
|
|
2209
|
+
try {
|
|
2210
|
+
const historyStr = history.slice(-30).join("\n");
|
|
2211
|
+
const isProxyMode = !!process.env.ARCALITY_API_URL;
|
|
2212
|
+
const endpointUrl = isProxyMode ? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy` : "https://api.anthropic.com/v1/messages";
|
|
2213
|
+
const headers = {
|
|
2214
|
+
"Content-Type": "application/json"
|
|
2215
|
+
};
|
|
2216
|
+
if (isProxyMode) {
|
|
2217
|
+
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
2218
|
+
const missionId = process.env.ARCALITY_MISSION_ID || "";
|
|
2219
|
+
if (missionId)
|
|
2220
|
+
headers["x-mission-id"] = missionId;
|
|
2221
|
+
} else {
|
|
2222
|
+
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
|
|
2223
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
2224
|
+
}
|
|
2225
|
+
const response = await fetch(endpointUrl, {
|
|
2226
|
+
method: "POST",
|
|
2227
|
+
headers,
|
|
2228
|
+
body: JSON.stringify({
|
|
2229
|
+
model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
|
|
2230
|
+
max_tokens: 1e3,
|
|
2231
|
+
system: `You are an elite QA Engineer. Your task is to transform a raw execution history into a clean, human-friendly English YAML Mission Card.
|
|
147
2232
|
|
|
148
2233
|
STRICT YAML STRUCTURE:
|
|
149
2234
|
Title: "Descriptive title for the test"
|
|
@@ -159,16 +2244,760 @@ RULES:
|
|
|
159
2244
|
1. Use professional English.
|
|
160
2245
|
2. The 'Steps' must be what actually happened in the history, but simplified for a human to read.
|
|
161
2246
|
3. 'Data' should extract values mentioned in the history (e.g., if you filled a name with 'Test-123', include it).
|
|
162
|
-
4. Output ONLY the YAML block. No preamble or explanations.`,
|
|
163
|
-
|
|
2247
|
+
4. Output ONLY the YAML block. No preamble or explanations.`,
|
|
2248
|
+
messages: [
|
|
2249
|
+
{ role: "user", content: `Original Mission: ${prompt}
|
|
2250
|
+
Starting URL: ${startUrl}
|
|
164
2251
|
|
|
165
2252
|
Execution history:
|
|
166
|
-
${
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
2253
|
+
${historyStr}` }
|
|
2254
|
+
],
|
|
2255
|
+
temperature: 0.1
|
|
2256
|
+
})
|
|
2257
|
+
});
|
|
2258
|
+
const data = await response.json();
|
|
2259
|
+
let yaml = data.content?.[0]?.text || null;
|
|
2260
|
+
if (yaml) {
|
|
2261
|
+
yaml = yaml.replace(/```yaml\n?/g, "").replace(/```/g, "").trim();
|
|
2262
|
+
}
|
|
2263
|
+
return yaml;
|
|
2264
|
+
} catch (e) {
|
|
2265
|
+
console.error("Error generating Smart YAML:", e);
|
|
2266
|
+
return null;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
|
|
2271
|
+
// src/services/securityScanner.ts
|
|
2272
|
+
var SecurityScanner = class {
|
|
2273
|
+
page;
|
|
2274
|
+
report = [];
|
|
2275
|
+
constructor(page) {
|
|
2276
|
+
this.page = page;
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Runs a series of automated scans against the current state of the page.
|
|
2280
|
+
* This is intended to be called at the end of a test run to aggregate findings.
|
|
2281
|
+
* @returns A promise that resolves to an array of found vulnerabilities.
|
|
2282
|
+
*/
|
|
2283
|
+
async runAllScans() {
|
|
2284
|
+
console.log("[QA-SEC] Starting comprehensive security scan...");
|
|
2285
|
+
const headerVulnerabilities = await audit_http_headers(this.page);
|
|
2286
|
+
this.report.push(...headerVulnerabilities);
|
|
2287
|
+
const storageVulnerabilities = await scan_sensitive_data_exposure(this.page);
|
|
2288
|
+
this.report.push(...storageVulnerabilities);
|
|
2289
|
+
await this.testOpenRedirect();
|
|
2290
|
+
console.log(`[QA-SEC] Comprehensive scan finished. Found ${this.report.length} potential vulnerabilities.`);
|
|
2291
|
+
return this.getReport();
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* A basic test for open redirect vulnerabilities on the current URL.
|
|
2295
|
+
*/
|
|
2296
|
+
async testOpenRedirect() {
|
|
2297
|
+
const url = new URL(this.page.url());
|
|
2298
|
+
const redirectParams = ["redirect", "next", "url", "returnTo", "dest"];
|
|
2299
|
+
for (const param of redirectParams) {
|
|
2300
|
+
if (url.searchParams.has(param)) {
|
|
2301
|
+
const originalValue = url.searchParams.get(param);
|
|
2302
|
+
const payload = "https://www.evil-redirect.com";
|
|
2303
|
+
url.searchParams.set(param, payload);
|
|
2304
|
+
console.log(`[QA-SEC] Testing Open Redirect on param: ${param}`);
|
|
2305
|
+
try {
|
|
2306
|
+
await this.page.goto(url.toString(), { waitUntil: "networkidle" });
|
|
2307
|
+
if (this.page.url().startsWith(payload)) {
|
|
2308
|
+
const vulnerability = {
|
|
2309
|
+
type: "confirmed_vulnerability",
|
|
2310
|
+
category: "Open Redirect",
|
|
2311
|
+
severity: "Medium",
|
|
2312
|
+
confidence: "High",
|
|
2313
|
+
exploitability: "confirmed",
|
|
2314
|
+
status: "detected",
|
|
2315
|
+
description: `The application is vulnerable to Open Redirect. The '${param}' parameter was manipulated to redirect the user to an external, malicious site.`,
|
|
2316
|
+
impact: "Attackers can use this to craft phishing links that appear to come from the trusted domain but redirect to malicious sites.",
|
|
2317
|
+
evidence: {
|
|
2318
|
+
vulnerableUrl: url.toString(),
|
|
2319
|
+
parameter: param,
|
|
2320
|
+
payload
|
|
2321
|
+
},
|
|
2322
|
+
reproductionSteps: [
|
|
2323
|
+
`Navigate to: ${url.toString()}`,
|
|
2324
|
+
"Observe that the page redirects to the payload domain."
|
|
2325
|
+
],
|
|
2326
|
+
remediation: "Validate all redirection URLs against a whitelist of allowed domains. Do not blindly redirect to user-supplied URLs.",
|
|
2327
|
+
classification: {
|
|
2328
|
+
cwe: "CWE-601"
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
this.report.push(vulnerability);
|
|
2332
|
+
await this.page.goBack();
|
|
2333
|
+
}
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
console.log(`[QA-SEC] Error during open redirect test, this might be expected if the navigation was blocked. Continuing scan.`);
|
|
2336
|
+
await this.page.goBack();
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Adds a vulnerability to the report. Can be used by other parts of the system
|
|
2343
|
+
* to feed into this scanner's report.
|
|
2344
|
+
* @param vuln - The vulnerability to add.
|
|
2345
|
+
*/
|
|
2346
|
+
addVulnerability(vuln) {
|
|
2347
|
+
this.report.push(vuln);
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Returns the final list of vulnerabilities.
|
|
2351
|
+
*/
|
|
2352
|
+
getReport() {
|
|
2353
|
+
const uniqueReport = [];
|
|
2354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2355
|
+
for (const vuln of this.report) {
|
|
2356
|
+
const key = `${vuln.category}|${vuln.description}|${vuln.evidence.selector || ""}`;
|
|
2357
|
+
if (!seen.has(key)) {
|
|
2358
|
+
uniqueReport.push(vuln);
|
|
2359
|
+
seen.add(key);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return uniqueReport;
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
|
|
2366
|
+
// tests/_helpers/agentic-runner.spec.ts
|
|
2367
|
+
var import_config = require("dotenv/config");
|
|
2368
|
+
var fs2 = __toESM(require("fs"));
|
|
2369
|
+
var path2 = __toESM(require("path"));
|
|
2370
|
+
var _sessionRuleCache = /* @__PURE__ */ new Set();
|
|
2371
|
+
function captureValidationRule(errorMessage, context) {
|
|
2372
|
+
const key = errorMessage.substring(0, 80).toLowerCase().trim();
|
|
2373
|
+
if (_sessionRuleCache.has(key))
|
|
2374
|
+
return;
|
|
2375
|
+
_sessionRuleCache.add(key);
|
|
2376
|
+
pushRule({
|
|
2377
|
+
rule_type: "VALIDATION",
|
|
2378
|
+
title: `Restricci\xF3n detectada: ${errorMessage.substring(0, 80)}`,
|
|
2379
|
+
description: `La aplicaci\xF3n rechaz\xF3 la acci\xF3n con el mensaje: "${errorMessage.substring(0, 300)}". Contexto: ${context.substring(0, 150)}`,
|
|
2380
|
+
severity: "important"
|
|
2381
|
+
}).catch(() => {
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
(0, import_test.test)("Arcality AI Runner", async ({ page }, testInfo) => {
|
|
2385
|
+
import_test.test.setTimeout(12e5);
|
|
2386
|
+
const contextDir = process.env.CONTEXT_DIR || "out";
|
|
2387
|
+
const activeConfig = process.env.ACTIVE_CONFIG || "Default";
|
|
2388
|
+
if (!process.env.BASE_URL && process.env[`${activeConfig}_URL`])
|
|
2389
|
+
process.env.BASE_URL = process.env[`${activeConfig}_URL`];
|
|
2390
|
+
if (!process.env.LOGIN_USER && process.env[`${activeConfig}_USER`])
|
|
2391
|
+
process.env.LOGIN_USER = process.env[`${activeConfig}_USER`];
|
|
2392
|
+
if (!process.env.LOGIN_PASSWORD && process.env[`${activeConfig}_PASS`])
|
|
2393
|
+
process.env.LOGIN_PASSWORD = process.env[`${activeConfig}_PASS`];
|
|
2394
|
+
await page.context().grantPermissions(["geolocation"]);
|
|
2395
|
+
await page.context().setGeolocation({ latitude: 19.4326, longitude: -99.1332 });
|
|
2396
|
+
const agent = new AIAgentHelper(page, contextDir, testInfo);
|
|
2397
|
+
const prompt = process.env.SMART_PROMPT || "Navega al inicio y verifica que el sitio cargue";
|
|
2398
|
+
const history = [];
|
|
2399
|
+
const urlHistory = [];
|
|
2400
|
+
const stepsData = [];
|
|
2401
|
+
const successKeywords = /exitosamente|creado correctamente|guardado correctamente|misión cumplida|registered successfully|created successfully|saved successfully|operación exitosa|registro guardado|successfully|correctly/i;
|
|
2402
|
+
const failureKeywords = /\berror\b|falló|failed|no se puede|cannot|unable|ya existe|already exists|inválido|invalid|incorrecto|incorrect|ligado|linked|depende|depends|duplicado|duplicate|repetido|repeated|reintentar|retry|missing|required|obligatorio|bad request|400|500/i;
|
|
2403
|
+
let aiMarkedSuccess = false;
|
|
2404
|
+
let hasCriticalError = false;
|
|
2405
|
+
let resultsSaved = false;
|
|
2406
|
+
let isFinished = false;
|
|
2407
|
+
let stepCount = 0;
|
|
2408
|
+
let guideStepCount = 0;
|
|
2409
|
+
let stepsDataBackup = [];
|
|
2410
|
+
const maxSteps = 30;
|
|
2411
|
+
let lastSeenSuccessToast = "";
|
|
2412
|
+
const saveMissionResults = () => {
|
|
2413
|
+
if (resultsSaved)
|
|
2414
|
+
return;
|
|
2415
|
+
resultsSaved = true;
|
|
2416
|
+
if (!fs2.existsSync(contextDir))
|
|
2417
|
+
fs2.mkdirSync(contextDir, { recursive: true });
|
|
2418
|
+
const finalSuccess = aiMarkedSuccess && !hasCriticalError;
|
|
2419
|
+
try {
|
|
2420
|
+
const memoryFile = path2.join(contextDir, "memoria-agente.json");
|
|
2421
|
+
let memories = [];
|
|
2422
|
+
if (fs2.existsSync(memoryFile)) {
|
|
2423
|
+
const content = fs2.readFileSync(memoryFile, "utf8").trim();
|
|
2424
|
+
if (content) {
|
|
2425
|
+
try {
|
|
2426
|
+
memories = JSON.parse(content);
|
|
2427
|
+
} catch (e) {
|
|
2428
|
+
console.warn("\u26A0\uFE0F Memoria corrupta, reiniciando...");
|
|
2429
|
+
memories = [];
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
const missionData = {
|
|
2434
|
+
prompt,
|
|
2435
|
+
steps: history,
|
|
2436
|
+
steps_data: stepsData,
|
|
2437
|
+
success: finalSuccess,
|
|
2438
|
+
error: hasCriticalError,
|
|
2439
|
+
timestamp: Date.now()
|
|
2440
|
+
};
|
|
2441
|
+
memories.push(missionData);
|
|
2442
|
+
fs2.writeFileSync(memoryFile, JSON.stringify(memories, null, 2));
|
|
2443
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${stepsData.length} pasos nuevos`);
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
console.error("\u274C Fall\xF3 al guardar memoria:", err);
|
|
2446
|
+
}
|
|
2447
|
+
try {
|
|
2448
|
+
const logPath = path2.join(contextDir, `agent-log-${Date.now()}.json`);
|
|
2449
|
+
fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError }, null, 2));
|
|
2450
|
+
} catch (err) {
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
let collectiveMemoryPersisted = false;
|
|
2454
|
+
const persistCollectiveMemory = async () => {
|
|
2455
|
+
if (collectiveMemoryPersisted)
|
|
2456
|
+
return;
|
|
2457
|
+
collectiveMemoryPersisted = true;
|
|
2458
|
+
const pid = process.env.ARCALITY_PROJECT_ID || "";
|
|
2459
|
+
const apiUrl = process.env.ARCALITY_API_URL || "";
|
|
2460
|
+
const apiKey = process.env.ARCALITY_API_KEY || "";
|
|
2461
|
+
if (!pid || !apiUrl || !apiKey) {
|
|
2462
|
+
console.warn(`[CollectiveMemory] \u26A0\uFE0F Vars faltantes \u2014 project_id='${pid}' api_url='${apiUrl}' api_key='${apiKey ? "OK" : "MISSING"}'`);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] Persistiendo conocimiento (project: ${pid})...`);
|
|
2466
|
+
try {
|
|
2467
|
+
const stepsNarrative = stepsData.map((s) => `${s.action_data?.action || "action"} "${s.componentName}" en ${s.url}`).join(" \u2192 ").substring(0, 500);
|
|
2468
|
+
const knowledgeId = await pushKnowledge({
|
|
2469
|
+
doc_type: "PROCESS",
|
|
2470
|
+
title: `Flujo exitoso: ${prompt.substring(0, 100)}`,
|
|
2471
|
+
content: stepsNarrative || prompt.substring(0, 500)
|
|
2472
|
+
});
|
|
2473
|
+
if (knowledgeId)
|
|
2474
|
+
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Knowledge guardado: ${knowledgeId}`);
|
|
2475
|
+
else
|
|
2476
|
+
console.warn(`[CollectiveMemory] \u26A0\uFE0F pushKnowledge retorn\xF3 null (revisar HTTP status arriba)`);
|
|
2477
|
+
const uniqueUrls = [...new Set(stepsData.map((s) => s.url).filter(Boolean))];
|
|
2478
|
+
if (uniqueUrls.length > 1) {
|
|
2479
|
+
const ruleId = await pushRule({
|
|
2480
|
+
rule_type: "NAVIGATION",
|
|
2481
|
+
title: `Flujo de navegaci\xF3n: ${prompt.substring(0, 80)}`,
|
|
2482
|
+
description: `Agente naveg\xF3 ${uniqueUrls.length} p\xE1ginas: ${uniqueUrls.join(" \u2192 ").substring(0, 400)}`,
|
|
2483
|
+
severity: "suggestion"
|
|
2484
|
+
});
|
|
2485
|
+
if (ruleId)
|
|
2486
|
+
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Regla NAVIGATION guardada: ${ruleId}`);
|
|
2487
|
+
}
|
|
2488
|
+
const submitStep = stepsData.find(
|
|
2489
|
+
(s) => s.action_data?.action === "click" && (s.componentName?.toLowerCase().includes("guardar") || s.componentName?.toLowerCase().includes("save") || s.componentName?.toLowerCase().includes("crear") || s.componentName?.toLowerCase().includes("registrar"))
|
|
2490
|
+
);
|
|
2491
|
+
if (submitStep) {
|
|
2492
|
+
const ruleId2 = await pushRule({
|
|
2493
|
+
rule_type: "UI_UX",
|
|
2494
|
+
title: `Patr\xF3n de guardado: ${prompt.substring(0, 60)}`,
|
|
2495
|
+
description: `Flujo exitoso incluy\xF3 guardado/creaci\xF3n. Pasos: ${stepsNarrative.substring(0, 400)}`,
|
|
2496
|
+
severity: "important"
|
|
2497
|
+
});
|
|
2498
|
+
if (ruleId2)
|
|
2499
|
+
console.log(`\u{1F9E0} [COLLECTIVE MEMORY] \u2705 Regla UI_UX guardada: ${ruleId2}`);
|
|
2500
|
+
}
|
|
2501
|
+
} catch (err) {
|
|
2502
|
+
console.warn(`[CollectiveMemory] \u274C Error inesperado en persistCollectiveMemory: ${err?.message}`);
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2505
|
+
console.log(`
|
|
2506
|
+
\u{1F916} [AGENTE] Iniciando misi\xF3n: "${prompt}"`);
|
|
2507
|
+
const base = process.env.BASE_URL || "";
|
|
2508
|
+
const target = process.env.TARGET_PATH || "/";
|
|
2509
|
+
if (process.env.LOGIN_USER && process.env.LOGIN_PASSWORD) {
|
|
2510
|
+
console.log(">>ARCALITY_STATUS>> \u{1F511} Realizando login autom\xE1tico...");
|
|
2511
|
+
const loginUrl = base + "/login";
|
|
2512
|
+
if (!loginUrl.startsWith("http"))
|
|
2513
|
+
throw new Error(`URL Inv\xE1lida: "${loginUrl}". Aseg\xFArate de configurar la Base URL con http://`);
|
|
2514
|
+
await page.goto(loginUrl);
|
|
2515
|
+
try {
|
|
2516
|
+
const userInp = page.locator('input[type="email"], input[name="email"], input[name="username"], [placeholder*="usuario" i], [placeholder*="correo" i], [placeholder*="email" i]').first();
|
|
2517
|
+
await userInp.waitFor({ state: "visible", timeout: 1e4 });
|
|
2518
|
+
const passInp = page.locator('input[type="password"], input[name="password"], [placeholder*="contrase\xF1a" i]').first();
|
|
2519
|
+
const subBtn = page.locator('button[type="submit"], button:has-text("Login"), button:has-text("Entrar"), [role="button"]:has-text("Entrar")').first();
|
|
2520
|
+
await userInp.click();
|
|
2521
|
+
await userInp.fill(process.env.LOGIN_USER || "");
|
|
2522
|
+
await passInp.click();
|
|
2523
|
+
await passInp.fill(process.env.LOGIN_PASSWORD || "");
|
|
2524
|
+
await page.waitForTimeout(500);
|
|
2525
|
+
await subBtn.click();
|
|
2526
|
+
await page.waitForURL((u) => !u.href.includes("/login"), { timeout: 15e3 }).catch(() => {
|
|
2527
|
+
});
|
|
2528
|
+
await page.waitForLoadState("networkidle").catch(() => {
|
|
2529
|
+
});
|
|
2530
|
+
} catch (e) {
|
|
2531
|
+
console.warn(` \u26A0\uFE0F Login autom\xE1tico omitido o ya autenticado.`);
|
|
2532
|
+
}
|
|
2533
|
+
if (target !== "/" && !page.url().includes(target)) {
|
|
2534
|
+
const tgtUrl = target.startsWith("http") ? target : base + target;
|
|
2535
|
+
if (!tgtUrl.startsWith("http"))
|
|
2536
|
+
throw new Error(`URL Inv\xE1lida: "${tgtUrl}"`);
|
|
2537
|
+
await page.goto(tgtUrl).catch(() => {
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
} else {
|
|
2541
|
+
const tgtUrl = target.startsWith("http") ? target : base + target;
|
|
2542
|
+
if (!tgtUrl.startsWith("http"))
|
|
2543
|
+
throw new Error(`URL Inv\xE1lida: "${tgtUrl}". Por favor proporciona una URL v\xE1lida incluyendo http:// o https://`);
|
|
2544
|
+
await page.goto(tgtUrl);
|
|
2545
|
+
}
|
|
2546
|
+
await page.waitForLoadState("networkidle");
|
|
2547
|
+
page.on("download", async (download) => {
|
|
2548
|
+
aiMarkedSuccess = true;
|
|
2549
|
+
isFinished = true;
|
|
2550
|
+
saveMissionResults();
|
|
2551
|
+
});
|
|
2552
|
+
const maxTotalIterations = maxSteps + 50;
|
|
2553
|
+
let totalIterations = 0;
|
|
2554
|
+
while (!isFinished && stepCount < maxSteps) {
|
|
2555
|
+
totalIterations++;
|
|
2556
|
+
if (totalIterations > maxTotalIterations) {
|
|
2557
|
+
console.error(`>>ARCALITY_STATUS>> \u{1F6D1} Safety limit: ${totalIterations} iteraciones totales alcanzadas. Abortando.`);
|
|
2558
|
+
hasCriticalError = true;
|
|
2559
|
+
break;
|
|
2560
|
+
}
|
|
2561
|
+
let containsError = false;
|
|
2562
|
+
await page.waitForTimeout(1e3);
|
|
2563
|
+
const currentUrl = page.url();
|
|
2564
|
+
const historyStr = history.join(" ").toLowerCase();
|
|
2565
|
+
const hadSubmitAction = historyStr.includes("guardar") || historyStr.includes("save") || historyStr.includes("registrar");
|
|
2566
|
+
const isMultiStep = prompt.toLowerCase().match(/después|luego|posteriormente|pestaña|tabla|doble clic|arrastra|paso|sección|modal|etapa/i);
|
|
2567
|
+
const allowRepeatedNames = prompt.toLowerCase().match(/modal|varias|distintos|diferentes|repetir|guardar dos veces/i) || isMultiStep;
|
|
2568
|
+
if (stepCount > 4 && hadSubmitAction && !isMultiStep) {
|
|
2569
|
+
const isNowOnList = !currentUrl.includes("/new") && !currentUrl.includes("/create") && !currentUrl.includes("/edit") && !currentUrl.includes("/details") && !currentUrl.includes("/alta");
|
|
2570
|
+
const wasInForm = historyStr.includes("nueva") || historyStr.includes("crear") || historyStr.includes("alta") || historyStr.includes("/new") || historyStr.includes("/create");
|
|
2571
|
+
if (wasInForm && isNowOnList) {
|
|
2572
|
+
const feedbackEls = await page.locator('.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .success-message, .mat-snack-bar-container').all();
|
|
2573
|
+
let foundSuccessToast = false;
|
|
2574
|
+
for (const el of feedbackEls) {
|
|
2575
|
+
if (await el.isVisible().catch(() => false)) {
|
|
2576
|
+
const txt = await el.innerText().catch(() => "");
|
|
2577
|
+
if (successKeywords.test(txt.toLowerCase())) {
|
|
2578
|
+
foundSuccessToast = true;
|
|
2579
|
+
console.log(`>>ARCALITY_STATUS>> \u2728 Auto-Success: Toast de \xE9xito detectado: "${txt.substring(0, 60)}"`);
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (foundSuccessToast) {
|
|
2585
|
+
console.log(`>>ARCALITY_STATUS>> \u2728 Auto-Success: Regreso a lista Y mensaje de \xE9xito detectado.`);
|
|
2586
|
+
aiMarkedSuccess = true;
|
|
2587
|
+
isFinished = true;
|
|
2588
|
+
saveMissionResults();
|
|
2589
|
+
break;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (stepCount === 1) {
|
|
2594
|
+
const initialToasts = await page.locator('.toast, .alert, [role="status"], [role="alert"]').all();
|
|
2595
|
+
for (const t of initialToasts) {
|
|
2596
|
+
if (await t.isVisible().catch(() => false)) {
|
|
2597
|
+
lastSeenSuccessToast = await t.innerText().catch(() => "");
|
|
2598
|
+
if (lastSeenSuccessToast)
|
|
2599
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4A1} Nota: Ignorando mensaje preexistente: "${lastSeenSuccessToast.substring(0, 30)}..."`);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const rawBody = await page.innerText("body").catch(() => "");
|
|
2604
|
+
const bodyTxt = rawBody.toLowerCase();
|
|
2605
|
+
const hasVisibleError = failureKeywords.test(bodyTxt);
|
|
2606
|
+
let response = null;
|
|
2607
|
+
if (!hasVisibleError) {
|
|
2608
|
+
response = await agent.getActionFromGuia(prompt, stepsData.length);
|
|
2609
|
+
} else {
|
|
2610
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error detectado en pantalla. Saltando Gu\xEDa para priorizar correcci\xF3n.`);
|
|
2611
|
+
if (stepsData.length > 0) {
|
|
2612
|
+
stepsDataBackup = [...stepsData];
|
|
2613
|
+
stepsData.length = 0;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (response) {
|
|
2617
|
+
const guideActionDesc = response.actions?.[0] ? `${response.actions[0].action} en "${response.actions[0].description || ""}"`.toLowerCase() : "";
|
|
2618
|
+
const recentHistory = history.slice(-4).map((h) => h.toLowerCase());
|
|
2619
|
+
const isGuideRepeating = guideActionDesc && recentHistory.some((h) => h.includes(guideActionDesc.substring(0, 30)));
|
|
2620
|
+
if (isGuideRepeating) {
|
|
2621
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa invalidada: la acci\xF3n "${guideActionDesc.substring(0, 50)}" ya fue intentada. Delegando a IA.`);
|
|
2622
|
+
response = null;
|
|
2623
|
+
} else {
|
|
2624
|
+
guideStepCount++;
|
|
2625
|
+
console.log(`>>ARCALITY_STATUS>> \u2728 Gu\xEDa de \xE9xito: Paso validado (Gu\xEDa #${guideStepCount}, NO consume turno IA)`);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
if (!response) {
|
|
2629
|
+
stepCount++;
|
|
2630
|
+
console.log(`>>ARCALITY_STATUS>> \u23F3 Turno IA ${stepCount} de ${maxSteps} (Gu\xEDa us\xF3 ${guideStepCount} turnos gratis)...`);
|
|
2631
|
+
response = await agent.askIA(prompt, history, stepCount);
|
|
2632
|
+
}
|
|
2633
|
+
console.log(`\u{1F9E0} Pensamiento: ${response.thought}`);
|
|
2634
|
+
if (!response.finish && history.length >= 6) {
|
|
2635
|
+
const lastSixActions = history.slice(-6);
|
|
2636
|
+
const clickActions = lastSixActions.filter((h) => h.includes("click en"));
|
|
2637
|
+
const fillActions = lastSixActions.filter((h) => h.includes("fill en"));
|
|
2638
|
+
const uniqueClicks = new Set(clickActions);
|
|
2639
|
+
const uniqueFills = new Set(fillActions);
|
|
2640
|
+
const isClickLoop = clickActions.length >= 4 && uniqueClicks.size <= 2;
|
|
2641
|
+
const isFillLoop = fillActions.length >= 4 && uniqueFills.size <= 2;
|
|
2642
|
+
if (isClickLoop || isFillLoop) {
|
|
2643
|
+
const loopType = isClickLoop ? "click" : "fill";
|
|
2644
|
+
const uniqueTargets = isClickLoop ? uniqueClicks : uniqueFills;
|
|
2645
|
+
console.warn(`
|
|
2646
|
+
\u26A0\uFE0F [LOOP DETECTOR] Patr\xF3n repetitivo de ${loopType} detectado:`);
|
|
2647
|
+
console.warn(` \xDAltimas acciones: ${lastSixActions.join(" \u2192 ")}`);
|
|
2648
|
+
console.warn(` El agente est\xE1 atrapado. Forzando terminaci\xF3n con sugerencia.`);
|
|
2649
|
+
response.thought = `\u{1F6D1} ERROR: Me he quedado atrapado en un bucle repetitivo intentando interactuar con: ${Array.from(uniqueTargets).join(", ")}. No puedo continuar la misi\xF3n de forma aut\xF3noma.
|
|
2650
|
+
|
|
2651
|
+
\u{1F4A1} REGLA DE QA: He detectado que mi acci\xF3n de ${loopType} no est\xE1 cambiando el estado de la p\xE1gina como esperaba. Esto podr\xEDa ser un bug de la aplicaci\xF3n o una limitaci\xF3n de mi percepci\xF3n actual.`;
|
|
2652
|
+
response.finish = true;
|
|
2653
|
+
response.actions = [];
|
|
2654
|
+
aiMarkedSuccess = false;
|
|
2655
|
+
hasCriticalError = true;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (response.actions && response.actions.length > 0) {
|
|
2659
|
+
for (const step of response.actions) {
|
|
2660
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F3AF} Acci\xF3n: ${step.action} en "${step.description || step.selector}"`);
|
|
2661
|
+
try {
|
|
2662
|
+
const frame = step.frameIdx !== void 0 && page.frames()[step.frameIdx] ? page.frames()[step.frameIdx] : page;
|
|
2663
|
+
const loc = step.selector ? frame.locator(step.selector).first() : null;
|
|
2664
|
+
const urlBeforeAction = page.url();
|
|
2665
|
+
if ((step.action === "click" || step.action === "double_click") && loc) {
|
|
2666
|
+
const desc = (step.description || "").toLowerCase();
|
|
2667
|
+
const isMenuTrigger = desc.includes("more_vert") || desc.includes("menu") || desc.includes("icono :") || desc.includes("dots") || desc.includes("opciones");
|
|
2668
|
+
const isSubmitAction = desc.includes("guardar") || desc.includes("save") || desc.includes("crear") || desc.includes("registrar") || desc.includes("enviar") || desc.includes("aceptar") || desc.includes("confirmar") || desc.includes("update");
|
|
2669
|
+
if (isSubmitAction) {
|
|
2670
|
+
const rawBodyText = await page.innerText("body").catch(() => "");
|
|
2671
|
+
const textLower = rawBodyText.toLowerCase();
|
|
2672
|
+
const isCriticalFailure = textLower.includes("error") || textLower.includes("fall\xF3") || textLower.includes("no se puede") || textLower.includes("ya existe") || textLower.includes("existe") || textLower.includes("inv\xE1lido") || textLower.includes("incorrecto") || textLower.includes("ligado") || textLower.includes("depende") || textLower.includes("duplicado") || textLower.includes("repetido") || textLower.includes("no encontrado") || textLower.includes("no existe") || textLower.includes("obligatorio") || textLower.includes("required");
|
|
2673
|
+
const isCriticalSuccess = textLower.includes("exitosamente") || textLower.includes("guardado") || textLower.includes("creado") || textLower.includes("success") || textLower.includes("correctamente") || textLower.includes("misi\xF3n cumplida");
|
|
2674
|
+
const isCriticalFeedback = isCriticalFailure || isCriticalSuccess;
|
|
2675
|
+
if (!isCriticalFeedback && step.type && ["span", "p", "label", "i", "svg", "img"].includes(step.type)) {
|
|
2676
|
+
const elementText = await loc.innerText().catch(() => "");
|
|
2677
|
+
const elementTextLower = elementText.toLowerCase();
|
|
2678
|
+
if (failureKeywords.test(elementTextLower)) {
|
|
2679
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Feedback de ERROR en elemento peque\xF1o: "${elementTextLower.substring(0, 50)}..."`);
|
|
2680
|
+
aiMarkedSuccess = false;
|
|
2681
|
+
hasCriticalError = true;
|
|
2682
|
+
isFinished = true;
|
|
2683
|
+
saveMissionResults();
|
|
2684
|
+
break;
|
|
2685
|
+
} else {
|
|
2686
|
+
const isSuccessMatch = successKeywords.test(elementTextLower);
|
|
2687
|
+
if (isSuccessMatch) {
|
|
2688
|
+
console.log(`>>ARCALITY_STATUS>> \u2728 \xC9xito visual en elemento peque\xF1o: "${elementTextLower.substring(0, 50)}..."`);
|
|
2689
|
+
aiMarkedSuccess = true;
|
|
2690
|
+
isFinished = true;
|
|
2691
|
+
hasCriticalError = false;
|
|
2692
|
+
saveMissionResults();
|
|
2693
|
+
break;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
const previousSubmit = history.find(
|
|
2698
|
+
(h) => h.toLowerCase().includes("click") && (h.toLowerCase().includes("guardar") || h.toLowerCase().includes("save") || h.toLowerCase().includes("crear") || h.toLowerCase().includes("registrar"))
|
|
2699
|
+
);
|
|
2700
|
+
if (previousSubmit && !allowRepeatedNames) {
|
|
2701
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Advertencia: Click persistente en bot\xF3n de acci\xF3n.`);
|
|
2702
|
+
history.push(`Turno ${stepCount}: Intentaste hacer click en "${desc}" nuevamente, pero la p\xE1gina sigue mostrando el mismo estado. \xBFHay alg\xFAn error visible que debas corregir antes?`);
|
|
2703
|
+
await page.waitForTimeout(1e3);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const lastAction = history[history.length - 1];
|
|
2707
|
+
if (lastAction && lastAction.includes(`click en "${desc}"`)) {
|
|
2708
|
+
const isDotsTrigger = desc.includes("dots") || desc.includes("vert") || desc.includes("menu") || desc.includes("opciones");
|
|
2709
|
+
const sameActionCount = history.filter((h) => h.includes(`click en "${desc}"`)).length;
|
|
2710
|
+
const toleranceLimit = allowRepeatedNames ? 3 : 1;
|
|
2711
|
+
if (isDotsTrigger && sameActionCount < 2) {
|
|
2712
|
+
console.log(` \u26A0\uFE0F Re-intentando click en men\xFA "${desc}"...`);
|
|
2713
|
+
} else if (allowRepeatedNames && sameActionCount < toleranceLimit) {
|
|
2714
|
+
console.log(` \u26A0\uFE0F Click repetido permitido por contexto de misi\xF3n en "${desc}"...`);
|
|
2715
|
+
} else {
|
|
2716
|
+
const bodyText = await page.innerText("body").catch(() => "");
|
|
2717
|
+
if (bodyText.match(/ligado|depende|no se puede/i)) {
|
|
2718
|
+
console.log(`
|
|
2719
|
+
\u2728 [ESTANCADO] El agente repite click pero el error ya es visible. Finalizando.`);
|
|
2720
|
+
isFinished = true;
|
|
2721
|
+
aiMarkedSuccess = true;
|
|
2722
|
+
break;
|
|
2723
|
+
}
|
|
2724
|
+
console.log(`
|
|
2725
|
+
\u{1F6D1} [AUTO-STOP] Bloqueando click duplicado en "${desc}". Forzando re-evaluaci\xF3n.`);
|
|
2726
|
+
history.push(`Turno ${stepCount}: Bloqueado click repetido en "${desc}" (Evitando bucle)`);
|
|
2727
|
+
await page.waitForTimeout(1e3);
|
|
2728
|
+
break;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
if (isMenuTrigger && history.some((h) => h.includes(desc) && h.includes("click"))) {
|
|
2732
|
+
console.log(` \u26A0\uFE0F El disparador "${desc}" ya fue clicado. Esperando estabilidad extra...`);
|
|
2733
|
+
await page.waitForTimeout(1500);
|
|
2734
|
+
}
|
|
2735
|
+
await loc.scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
2736
|
+
});
|
|
2737
|
+
if (step.action === "double_click") {
|
|
2738
|
+
await loc.dblclick({ timeout: 1e4, force: true });
|
|
2739
|
+
} else {
|
|
2740
|
+
if (desc.includes("toast") || desc.includes("close")) {
|
|
2741
|
+
await loc.click({ timeout: 2e3, force: true }).catch(() => console.log(" \u26A0\uFE0F No se pudo cerrar toast, ignorando..."));
|
|
2742
|
+
} else {
|
|
2743
|
+
await loc.click({ timeout: 1e4, force: true });
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (isMenuTrigger) {
|
|
2747
|
+
console.log(" \u23F3 Abriendo men\xFA, esperando 1.5s para renderizado...");
|
|
2748
|
+
await page.waitForTimeout(1500);
|
|
2749
|
+
}
|
|
2750
|
+
const actsLikeSubmit = desc.includes("guardar") || desc.includes("save") || desc.includes("crear") || desc.includes("registrar");
|
|
2751
|
+
if (actsLikeSubmit) {
|
|
2752
|
+
console.log(" \u23F3 Esperando transici\xF3n post-guardado...");
|
|
2753
|
+
try {
|
|
2754
|
+
await page.waitForLoadState("networkidle", { timeout: 8e3 });
|
|
2755
|
+
} catch (e) {
|
|
2756
|
+
console.log(" \u26A0\uFE0F Timeout esperando red, continuando...");
|
|
2757
|
+
}
|
|
2758
|
+
const postSubmitFeedback = await page.locator('.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .success-message, .mat-snack-bar-container').all();
|
|
2759
|
+
let foundSuccessPost = false;
|
|
2760
|
+
for (const fb of postSubmitFeedback) {
|
|
2761
|
+
if (await fb.isVisible().catch(() => false)) {
|
|
2762
|
+
const fbText = await fb.innerText().catch(() => "");
|
|
2763
|
+
const postSuccessKw = /exitosamente|creado correctamente|guardado correctamente|operación exitosa|registro guardado|saved successfully|created successfully/i;
|
|
2764
|
+
if (postSuccessKw.test(fbText)) {
|
|
2765
|
+
console.log(`>>ARCALITY_STATUS>> \u2728 \xC9xito visual detectado en toast: "${fbText.substring(0, 60)}"`);
|
|
2766
|
+
aiMarkedSuccess = true;
|
|
2767
|
+
isFinished = true;
|
|
2768
|
+
saveMissionResults();
|
|
2769
|
+
foundSuccessPost = true;
|
|
2770
|
+
break;
|
|
2771
|
+
} else if (failureKeywords.test(fbText.toLowerCase())) {
|
|
2772
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4DA} [BUSINESS RULE] Error de validaci\xF3n capturado: "${fbText.substring(0, 80)}"`);
|
|
2773
|
+
captureValidationRule(fbText.trim(), `Acci\xF3n: click en "${desc}" en ${page.url()}`);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
if (foundSuccessPost)
|
|
2778
|
+
break;
|
|
2779
|
+
const currentUrl2 = page.url();
|
|
2780
|
+
const isBackOnList = urlBeforeAction !== currentUrl2 && !currentUrl2.includes("/new") && !currentUrl2.includes("/edit") && !currentUrl2.includes("/create");
|
|
2781
|
+
if (isBackOnList) {
|
|
2782
|
+
console.log(`
|
|
2783
|
+
\u2728 [AUTO-SUCCESS] Navegaci\xF3n detectada tras guardar (${urlBeforeAction} -> ${currentUrl2}). Confirmando...`);
|
|
2784
|
+
await page.waitForTimeout(2e3);
|
|
2785
|
+
const postNavAlerts = await page.locator(
|
|
2786
|
+
'.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .error-message, .mat-snack-bar-container'
|
|
2787
|
+
).all();
|
|
2788
|
+
let hasPostNavError = false;
|
|
2789
|
+
for (const alertEl of postNavAlerts) {
|
|
2790
|
+
if (await alertEl.isVisible().catch(() => false)) {
|
|
2791
|
+
const alertTxt = await alertEl.innerText().catch(() => "");
|
|
2792
|
+
if (failureKeywords.test(alertTxt.toLowerCase())) {
|
|
2793
|
+
hasPostNavError = true;
|
|
2794
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en alerta post-guardado: "${alertTxt.substring(0, 60)}"`);
|
|
2795
|
+
break;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
if (!hasPostNavError) {
|
|
2800
|
+
aiMarkedSuccess = true;
|
|
2801
|
+
isFinished = true;
|
|
2802
|
+
await persistCollectiveMemory();
|
|
2803
|
+
saveMissionResults();
|
|
2804
|
+
break;
|
|
2805
|
+
} else {
|
|
2806
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Se detect\xF3 navegaci\xF3n pero hay un ERROR en alerta/toast visible.`);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
} else if (desc.includes("siguiente") || desc.includes("next")) {
|
|
2810
|
+
await page.waitForTimeout(1e3);
|
|
2811
|
+
} else {
|
|
2812
|
+
await page.waitForTimeout(500);
|
|
2813
|
+
}
|
|
2814
|
+
} else if (step.action === "fill" && loc && step.value) {
|
|
2815
|
+
const isFile = await loc.evaluate((el) => el.tagName === "INPUT" && el.type === "file").catch(() => false);
|
|
2816
|
+
if (isFile) {
|
|
2817
|
+
console.log(` \u{1F4C2} Detectado input de archivo. Subiendo: "${step.value}"`);
|
|
2818
|
+
const filePath = step.value.replace(/^"|"$/g, "").trim();
|
|
2819
|
+
await loc.setInputFiles(filePath);
|
|
2820
|
+
} else {
|
|
2821
|
+
await loc.click({ timeout: 5e3 }).catch(() => {
|
|
2822
|
+
});
|
|
2823
|
+
await loc.fill(step.value, { timeout: 1e4, force: true });
|
|
2824
|
+
await loc.press("Tab").catch(() => {
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
await page.waitForTimeout(500);
|
|
2828
|
+
} else if (step.action === "wait") {
|
|
2829
|
+
await page.waitForTimeout(3e3);
|
|
2830
|
+
}
|
|
2831
|
+
const descForHistory = (step.description || step.selector || "").substring(0, 80);
|
|
2832
|
+
const valStr = step.value ? ` con valor "${step.value}"` : "";
|
|
2833
|
+
history.push(`Turno ${stepCount}: ${step.action} en "${descForHistory}"${valStr}`);
|
|
2834
|
+
urlHistory.push(page.url());
|
|
2835
|
+
const compName = (step.description || "").substring(0, 80);
|
|
2836
|
+
if (compName.length < 100) {
|
|
2837
|
+
stepsData.push({
|
|
2838
|
+
url: urlBeforeAction,
|
|
2839
|
+
// USAR URL CAPTURADA ANTES
|
|
2840
|
+
url_normalized: urlBeforeAction.replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/"),
|
|
2841
|
+
componentName: compName,
|
|
2842
|
+
componentType: step.type,
|
|
2843
|
+
action_data: { action: step.action, value: step.value },
|
|
2844
|
+
finish: response.finish
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
} catch (e) {
|
|
2848
|
+
console.warn(` \u26A0\uFE0F Acci\xF3n fallida: ${e.message}`);
|
|
2849
|
+
history.push(`Turno ${stepCount}: \u274C FALL\xD3 ${step.action} en "${step.description}" - Error: ${e.message}. REGLA MANDATORIA: Debes investigar la causa usando 'inspect_element_details' o 'capture_console_errors' antes de cualquier otro paso.`);
|
|
2850
|
+
containsError = true;
|
|
2851
|
+
const errorCount = history.filter((h) => h.includes("FALL\xD3") && h.includes(step.description)).length;
|
|
2852
|
+
if (errorCount >= 2) {
|
|
2853
|
+
console.error(`>>ARCALITY_STATUS>> \u{1F6D1} Demasiados fallos t\xE9cnicos en este elemento "${step.description}". Abortando misi\xF3n.`);
|
|
2854
|
+
hasCriticalError = true;
|
|
2855
|
+
isFinished = true;
|
|
2856
|
+
break;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
if (response.finish) {
|
|
2862
|
+
if (!containsError) {
|
|
2863
|
+
console.log(">>ARCALITY_STATUS>> \u2705 El agente solicit\xF3 finalizar misi\xF3n.");
|
|
2864
|
+
isFinished = true;
|
|
2865
|
+
if (!response.actions || response.actions.length === 0) {
|
|
2866
|
+
stepsData.push({
|
|
2867
|
+
url: page.url(),
|
|
2868
|
+
url_normalized: page.url().replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/"),
|
|
2869
|
+
componentName: "MISSION_END",
|
|
2870
|
+
componentType: "FINISH_SIGNAL",
|
|
2871
|
+
action_data: { action: "finish" },
|
|
2872
|
+
finish: true
|
|
2873
|
+
});
|
|
2874
|
+
} else {
|
|
2875
|
+
if (stepsData.length > 0) {
|
|
2876
|
+
stepsData[stepsData.length - 1].finish = true;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
const finishFeedbackEls = await page.locator('.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .mat-snack-bar-container, .error-message').all();
|
|
2880
|
+
let hasVisibleFailure = false;
|
|
2881
|
+
for (const el of finishFeedbackEls) {
|
|
2882
|
+
if (await el.isVisible().catch(() => false)) {
|
|
2883
|
+
const txt = await el.innerText().catch(() => "");
|
|
2884
|
+
if (failureKeywords.test(txt.toLowerCase())) {
|
|
2885
|
+
hasVisibleFailure = true;
|
|
2886
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error detectado en toast/alerta: "${txt.substring(0, 60)}"`);
|
|
2887
|
+
break;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
if (!hasVisibleFailure) {
|
|
2892
|
+
aiMarkedSuccess = true;
|
|
2893
|
+
hasCriticalError = false;
|
|
2894
|
+
saveMissionResults();
|
|
2895
|
+
if (stepsData.length === 0 && stepsDataBackup.length > 0) {
|
|
2896
|
+
stepsData.push(...stepsDataBackup);
|
|
2897
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F504} Gu\xEDa de \xC9xito restaurada (${stepsDataBackup.length} pasos recuperados).`);
|
|
2898
|
+
}
|
|
2899
|
+
await persistCollectiveMemory();
|
|
2900
|
+
} else {
|
|
2901
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F El agente intent\xF3 finalizar pero se detect\xF3 un ERROR en alert/toast.`);
|
|
2902
|
+
aiMarkedSuccess = false;
|
|
2903
|
+
isFinished = false;
|
|
2904
|
+
history.push(`Turno ${stepCount}: El agente intent\xF3 finalizar pero el sistema detect\xF3 un error en un toast/alerta visible.`);
|
|
2905
|
+
}
|
|
2906
|
+
} else {
|
|
2907
|
+
console.warn(">>ARCALITY_STATUS>> \u26A0\uFE0F El agente intent\xF3 finalizar pero hubo fallos en el turno. Continuando para investigaci\xF3n...");
|
|
2908
|
+
isFinished = false;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
const feedbackSelector = '.toast, .alert, .message, div[role="status"], .error-message, [class*="alert"], [class*="toast"], .status-message';
|
|
2912
|
+
const feedbackLocator = page.locator(feedbackSelector);
|
|
2913
|
+
if (stepCount > 2) {
|
|
2914
|
+
const allFeedback = await feedbackLocator.all();
|
|
2915
|
+
for (const fb of allFeedback) {
|
|
2916
|
+
if (await fb.isVisible()) {
|
|
2917
|
+
const rawTxt = await fb.innerText();
|
|
2918
|
+
const txt = rawTxt.toLowerCase();
|
|
2919
|
+
if (failureKeywords.test(txt)) {
|
|
2920
|
+
console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Feedback de ERROR detectado: "${rawTxt.substring(0, 60)}..."`);
|
|
2921
|
+
aiMarkedSuccess = false;
|
|
2922
|
+
captureValidationRule(rawTxt.trim(), `URL: ${page.url()}`);
|
|
2923
|
+
if (stepsData.length > 0) {
|
|
2924
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F504} Invalidando Gu\xEDa de \xC9xito por feedback de error.`);
|
|
2925
|
+
stepsDataBackup = [...stepsData];
|
|
2926
|
+
stepsData.length = 0;
|
|
2927
|
+
}
|
|
2928
|
+
} else if (successKeywords.test(txt)) {
|
|
2929
|
+
if (txt !== lastSeenSuccessToast) {
|
|
2930
|
+
console.log(`>>ARCALITY_STATUS>> \u2728 \xC9xito visual detectado: "${txt.substring(0, 50)}..."`);
|
|
2931
|
+
aiMarkedSuccess = true;
|
|
2932
|
+
isFinished = true;
|
|
2933
|
+
hasCriticalError = false;
|
|
2934
|
+
await persistCollectiveMemory();
|
|
2935
|
+
saveMissionResults();
|
|
2936
|
+
break;
|
|
2937
|
+
} else {
|
|
2938
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4A1} Mensaje de \xE9xito persistente ignorado.`);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
if (aiMarkedSuccess) {
|
|
2946
|
+
hasCriticalError = false;
|
|
2947
|
+
await persistCollectiveMemory();
|
|
2948
|
+
console.log("\u270D\uFE0F [HUMANIZANDO] Generando resumen de \xE9xito para el reporte...");
|
|
2949
|
+
const summary = await agent.humanizeMissionResult(prompt, history);
|
|
2950
|
+
if (summary) {
|
|
2951
|
+
await testInfo.attach("success_summary", {
|
|
2952
|
+
body: Buffer.from(summary, "utf-8"),
|
|
2953
|
+
contentType: "text/plain"
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
console.log("\u{1F916} [SMART-YAML] Generating mission card...");
|
|
2957
|
+
const smartYaml = await agent.summarizeMissionToYaml(prompt, history, target);
|
|
2958
|
+
if (smartYaml) {
|
|
2959
|
+
const yamlPath = path2.join(contextDir, "last-mission-smart.yaml");
|
|
2960
|
+
fs2.writeFileSync(yamlPath, smartYaml);
|
|
2961
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4DD} Smart Mission Card generated.`);
|
|
2962
|
+
await testInfo.attach("smart_mission_card", {
|
|
2963
|
+
body: Buffer.from(smartYaml, "utf-8"),
|
|
2964
|
+
contentType: "text/yaml"
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
try {
|
|
2969
|
+
const scanner = new SecurityScanner(page);
|
|
2970
|
+
const securityReport = await scanner.runAllScans();
|
|
2971
|
+
if (securityReport && securityReport.length > 0) {
|
|
2972
|
+
await testInfo.attach("security_report", {
|
|
2973
|
+
body: JSON.stringify(securityReport, null, 2),
|
|
2974
|
+
contentType: "application/json"
|
|
2975
|
+
});
|
|
2976
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F Escaneo de seguridad completado. ${securityReport.length} problemas encontrados.`);
|
|
2977
|
+
try {
|
|
2978
|
+
for (const v of securityReport) {
|
|
2979
|
+
const sev = v.severity || "Info";
|
|
2980
|
+
const mappedSeverity = sev === "Critical" ? "critical" : sev === "High" ? "important" : "suggestion";
|
|
2981
|
+
await pushRule({
|
|
2982
|
+
rule_type: "SECURITY",
|
|
2983
|
+
title: `Security: ${v.category} (${v.severity})`,
|
|
2984
|
+
description: `${v.description}
|
|
2985
|
+
Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
|
|
2986
|
+
severity: mappedSeverity
|
|
2987
|
+
}).catch(() => {
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
} catch (e) {
|
|
2991
|
+
console.warn(">>ARCALITY_STATUS>> \u26A0\uFE0F Fall\xF3 persistir hallazgos de seguridad en Memoria Colectiva.");
|
|
2992
|
+
}
|
|
2993
|
+
} else {
|
|
2994
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F Escaneo de seguridad completado. No se encontraron problemas.`);
|
|
2995
|
+
}
|
|
2996
|
+
} catch (e) {
|
|
2997
|
+
console.error("Error in Security Scan:", e.message);
|
|
2998
|
+
}
|
|
2999
|
+
saveMissionResults();
|
|
3000
|
+
if (!aiMarkedSuccess) {
|
|
3001
|
+
throw new Error("Misi\xF3n no completada o finalizada con errores.");
|
|
3002
|
+
}
|
|
3003
|
+
});
|