@arcadialdev/arcality 2.2.0
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/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
- package/.agents/skills/frontend-design/LICENSE.txt +177 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
- package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
- package/.agents/skills/playwright-best-practices/README.md +147 -0
- package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
- package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
- package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
- package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
- package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
- package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
- package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
- package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
- package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
- package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
- package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
- package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
- package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
- package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
- package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
- package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
- package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
- package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
- package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
- package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
- package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
- package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
- package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
- package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
- package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
- package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
- package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
- package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
- package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
- package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
- package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
- package/.env.example +21 -0
- package/README.md +30 -0
- package/bin/arcality.mjs +86 -0
- package/package.json +66 -0
- package/playwright.config.ts +12 -0
- package/scripts/cleanup-qmsdev.mjs +63 -0
- package/scripts/discover-view.mjs +52 -0
- package/scripts/extract-view.mjs +64 -0
- package/scripts/gen-and-run.mjs +838 -0
- package/scripts/init.mjs +290 -0
- package/scripts/migrate-to-central-out.mjs +157 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/rebrand-report.mjs +241 -0
- package/scripts/setup.mjs +166 -0
- package/src/KnowledgeService.ts +239 -0
- package/src/arcalityClient.mjs +266 -0
- package/src/configLoader.mjs +179 -0
- package/src/configManager.mjs +172 -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/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +490 -0
- package/tests/_helpers/agentic-runner.spec.ts +741 -0
- package/tests/_helpers/ai-agent-helper.ts +1573 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
package/src/envSetup.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
|
|
5
|
+
export class UserCancelledError extends Error {
|
|
6
|
+
constructor(message = 'Usuario canceló la operación') {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'UserCancelledError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type EnvValues = {
|
|
13
|
+
BASE_URL?: string;
|
|
14
|
+
LOGIN_USER?: string;
|
|
15
|
+
LOGIN_PASSWORD?: string;
|
|
16
|
+
ARCALITY_PROJECT_ID?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface ArcalityConfigFile {
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
projectId?: string;
|
|
22
|
+
project?: {
|
|
23
|
+
name?: string;
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
frameworkDetected?: string;
|
|
26
|
+
};
|
|
27
|
+
auth?: {
|
|
28
|
+
username?: string;
|
|
29
|
+
password?: string;
|
|
30
|
+
};
|
|
31
|
+
runtime?: {
|
|
32
|
+
yamlOutputDir?: string;
|
|
33
|
+
reuseSuccessfulYamls?: boolean;
|
|
34
|
+
singleConfigurationMode?: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function envPath(): string {
|
|
39
|
+
return path.join(process.cwd(), '.env');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function arcalityConfigPath(): string {
|
|
43
|
+
return path.join(process.cwd(), 'arcality.config');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function escapeEnvValue(v: string): string {
|
|
47
|
+
const needsQuotes = /[\s"'`\\]/.test(v);
|
|
48
|
+
const cleaned = v.replace(/"/g, '\\"');
|
|
49
|
+
return needsQuotes ? `"${cleaned}"` : cleaned;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseDotEnv(content: string): Record<string, string> {
|
|
53
|
+
const out: Record<string, string> = {};
|
|
54
|
+
for (const line of content.split(/\r?\n/)) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
57
|
+
|
|
58
|
+
const idx = trimmed.indexOf('=');
|
|
59
|
+
if (idx === -1) continue;
|
|
60
|
+
|
|
61
|
+
const k = trimmed.slice(0, idx).trim();
|
|
62
|
+
let v = trimmed.slice(idx + 1).trim();
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
(v.startsWith('"') && v.endsWith('"')) ||
|
|
66
|
+
(v.startsWith("'") && v.endsWith("'"))
|
|
67
|
+
) {
|
|
68
|
+
v = v.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
out[k] = v;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readExistingEnv(): Record<string, string> {
|
|
76
|
+
const p = envPath();
|
|
77
|
+
if (!fs.existsSync(p)) return {};
|
|
78
|
+
return parseDotEnv(fs.readFileSync(p, 'utf8'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Loads arcality.config if it exists.
|
|
83
|
+
*/
|
|
84
|
+
function loadArcalityConfig(): ArcalityConfigFile | null {
|
|
85
|
+
const p = arcalityConfigPath();
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.existsSync(p)) return null;
|
|
88
|
+
let raw = fs.readFileSync(p, 'utf8');
|
|
89
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
90
|
+
return JSON.parse(raw);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeEnv(values: EnvValues): void {
|
|
97
|
+
const existing = readExistingEnv();
|
|
98
|
+
const merged = { ...existing, ...values };
|
|
99
|
+
|
|
100
|
+
const lines = [
|
|
101
|
+
'# Auto-generado por Arcality testRunner',
|
|
102
|
+
`BASE_URL=${escapeEnvValue(merged.BASE_URL ?? '')}`,
|
|
103
|
+
`LOGIN_USER=${escapeEnvValue(merged.LOGIN_USER ?? '')}`,
|
|
104
|
+
`LOGIN_PASSWORD=${escapeEnvValue(merged.LOGIN_PASSWORD ?? '')}`,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
if (merged.ARCALITY_PROJECT_ID) {
|
|
108
|
+
lines.push(`ARCALITY_PROJECT_ID=${escapeEnvValue(merged.ARCALITY_PROJECT_ID)}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push('');
|
|
111
|
+
|
|
112
|
+
fs.writeFileSync(envPath(), lines.join('\n'), 'utf8');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isValidBaseUrl(url: string): boolean {
|
|
116
|
+
try {
|
|
117
|
+
const u = new URL(url);
|
|
118
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Ensures environment is configured for test execution.
|
|
126
|
+
* In single-config mode, reads from arcality.config first.
|
|
127
|
+
* Falls back to .env prompts for backward compatibility.
|
|
128
|
+
*/
|
|
129
|
+
export async function ensureEnvInteractive(): Promise<void> {
|
|
130
|
+
// Try to load arcality.config first
|
|
131
|
+
const arcalityConfig = loadArcalityConfig();
|
|
132
|
+
|
|
133
|
+
if (arcalityConfig) {
|
|
134
|
+
// Inject config values into process.env for the current session
|
|
135
|
+
if (arcalityConfig.project?.baseUrl) {
|
|
136
|
+
process.env.BASE_URL = arcalityConfig.project.baseUrl;
|
|
137
|
+
}
|
|
138
|
+
if (arcalityConfig.auth?.username) {
|
|
139
|
+
process.env.LOGIN_USER = arcalityConfig.auth.username;
|
|
140
|
+
}
|
|
141
|
+
if (arcalityConfig.auth?.password) {
|
|
142
|
+
process.env.LOGIN_PASSWORD = arcalityConfig.auth.password;
|
|
143
|
+
}
|
|
144
|
+
if (arcalityConfig.projectId) {
|
|
145
|
+
process.env.ARCALITY_PROJECT_ID = arcalityConfig.projectId;
|
|
146
|
+
}
|
|
147
|
+
if (arcalityConfig.apiKey) {
|
|
148
|
+
process.env.ARCALITY_API_KEY = arcalityConfig.apiKey;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// All good — no prompts needed
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Fallback: prompt for missing .env values (legacy mode)
|
|
156
|
+
const existing = readExistingEnv();
|
|
157
|
+
const questions: prompts.PromptObject[] = [];
|
|
158
|
+
|
|
159
|
+
if (!existing.BASE_URL) {
|
|
160
|
+
questions.push({
|
|
161
|
+
type: 'text',
|
|
162
|
+
name: 'BASE_URL',
|
|
163
|
+
message: 'Base URL (ej: http://localhost:3000):',
|
|
164
|
+
initial: 'http://localhost:3000',
|
|
165
|
+
validate: (v: string) =>
|
|
166
|
+
isValidBaseUrl(v) ? true : 'URL inválida (usa http/https)',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!existing.LOGIN_USER) {
|
|
171
|
+
questions.push({
|
|
172
|
+
type: 'text',
|
|
173
|
+
name: 'LOGIN_USER',
|
|
174
|
+
message: 'Usuario:',
|
|
175
|
+
validate: (v: string) =>
|
|
176
|
+
v?.trim().length ? true : 'Usuario requerido',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!existing.LOGIN_PASSWORD) {
|
|
181
|
+
questions.push({
|
|
182
|
+
type: 'password',
|
|
183
|
+
name: 'LOGIN_PASSWORD',
|
|
184
|
+
message: 'Contraseña:',
|
|
185
|
+
validate: (v: string) =>
|
|
186
|
+
v?.length ? true : 'Contraseña requerida',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (questions.length > 0) {
|
|
191
|
+
const res = await prompts(questions, {
|
|
192
|
+
onCancel: () => {
|
|
193
|
+
throw new UserCancelledError();
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const merged: EnvValues = {
|
|
198
|
+
BASE_URL: res.BASE_URL ?? existing.BASE_URL,
|
|
199
|
+
LOGIN_USER: res.LOGIN_USER ?? existing.LOGIN_USER,
|
|
200
|
+
LOGIN_PASSWORD: res.LOGIN_PASSWORD ?? existing.LOGIN_PASSWORD,
|
|
201
|
+
};
|
|
202
|
+
writeEnv(merged);
|
|
203
|
+
console.log('Archivo .env actualizado.');
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { showBanner } from './consoleBanner';
|
|
3
|
+
import { promptAndRunPlaywrightTests } from './testRunner';
|
|
4
|
+
import { KnowledgeService } from './KnowledgeService';
|
|
5
|
+
import { ArcalityClient } from './arcalityClient';
|
|
6
|
+
import { loadConfig } from './configLoader';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
showBanner();
|
|
10
|
+
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
const arcalityClient = new ArcalityClient(config.ARCALITY_API_KEY);
|
|
13
|
+
const knowledgeService = KnowledgeService.getInstance();
|
|
14
|
+
|
|
15
|
+
await promptAndRunPlaywrightTests(arcalityClient, knowledgeService);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main().catch((err) => {
|
|
19
|
+
// Normal user cancellation is not an error
|
|
20
|
+
if (err instanceof Error && err.name === 'UserCancelledError') {
|
|
21
|
+
process.exit(130);
|
|
22
|
+
}
|
|
23
|
+
console.error(err);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface ProjectContext {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
framework: 'next' | 'vite' | 'cra' | 'unknown';
|
|
8
|
+
scripts: string[];
|
|
9
|
+
dependencies: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getProjectContext(projectRoot: string): ProjectContext {
|
|
13
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
14
|
+
const context: ProjectContext = {
|
|
15
|
+
name: 'Arcality Project',
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
framework: 'unknown',
|
|
18
|
+
scripts: [],
|
|
19
|
+
dependencies: []
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(pkgPath)) return context;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
26
|
+
context.name = pkg.name || context.name;
|
|
27
|
+
context.version = pkg.version || context.version;
|
|
28
|
+
|
|
29
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
30
|
+
context.dependencies = Object.keys(deps);
|
|
31
|
+
context.scripts = pkg.scripts ? Object.keys(pkg.scripts) : [];
|
|
32
|
+
|
|
33
|
+
if (deps['next']) context.framework = 'next';
|
|
34
|
+
else if (deps['vite']) context.framework = 'vite';
|
|
35
|
+
else if (deps['react-scripts']) context.framework = 'cra';
|
|
36
|
+
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Ignorar errores de lectura
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return context;
|
|
42
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* collectiveMemoryService.ts
|
|
3
|
+
* Servicio de escritura hacia la Memoria Colectiva del backend de Arcality.
|
|
4
|
+
*
|
|
5
|
+
* REGLAS CRÍTICAS:
|
|
6
|
+
* - NUNCA interrumpe el flujo principal (todo en try/catch silencioso)
|
|
7
|
+
* - NUNCA envía datos sensibles (passwords, tokens, PII)
|
|
8
|
+
* - NUNCA duplica fields (valida contra /portal/context primero)
|
|
9
|
+
* - SIEMPRE usa snake_case según JsonNamingPolicy.SnakeCaseLower del servidor
|
|
10
|
+
* - NUNCA envía project_id vacío o con GUID cero
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const getBase = () => process.env.ARCALITY_API_URL ? `${process.env.ARCALITY_API_URL}/api/v1` : null;
|
|
14
|
+
const getKey = () => process.env.ARCALITY_API_KEY || '';
|
|
15
|
+
const getPid = () => process.env.ARCALITY_PROJECT_ID || '';
|
|
16
|
+
|
|
17
|
+
const EMPTY_GUID = '00000000-0000-0000-0000-000000000000';
|
|
18
|
+
|
|
19
|
+
function isConfigured(): boolean {
|
|
20
|
+
const base = getBase();
|
|
21
|
+
const key = getKey();
|
|
22
|
+
const pid = getPid();
|
|
23
|
+
|
|
24
|
+
if (!base || !key || !pid || pid === EMPTY_GUID) {
|
|
25
|
+
// Silencioso — no loguear en cada percepción para no hacer ruido
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── 1. REGLAS DE UI / NEGOCIO ──────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export type RuleType = 'UI_UX' | 'VALIDATION' | 'NAVIGATION' | 'BUSINESS';
|
|
34
|
+
export type Severity = 'critical' | 'important' | 'suggestion';
|
|
35
|
+
|
|
36
|
+
export async function pushRule(rule: {
|
|
37
|
+
rule_type: RuleType;
|
|
38
|
+
title: string;
|
|
39
|
+
description: string;
|
|
40
|
+
severity?: Severity;
|
|
41
|
+
}): Promise<string | null> {
|
|
42
|
+
if (!isConfigured()) return null;
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`${getBase()}/portal/rules`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'x-api-key': getKey()
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
project_id: getPid(),
|
|
52
|
+
rule_type: rule.rule_type,
|
|
53
|
+
title: rule.title.substring(0, 100),
|
|
54
|
+
description: rule.description.substring(0, 500),
|
|
55
|
+
severity: rule.severity ?? 'important',
|
|
56
|
+
source: 'agent'
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
console.warn(`[CollectiveMemory] pushRule HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
console.log(`\x1b[32m[CollectiveMemory] ✅ Regla guardada: "${rule.title}"\x1b[0m`);
|
|
67
|
+
return data.id ?? null;
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
console.warn(`[CollectiveMemory] pushRule error: ${err?.message}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── 2. CATÁLOGO DE CAMPOS ──────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export type FieldType = 'email' | 'password' | 'text' | 'number' | 'select' | 'checkbox' | 'textarea' | 'date';
|
|
77
|
+
|
|
78
|
+
export async function pushField(field: {
|
|
79
|
+
field_identifier: string;
|
|
80
|
+
field_type?: FieldType | string;
|
|
81
|
+
is_required?: boolean;
|
|
82
|
+
validation_rule?: string;
|
|
83
|
+
page_id?: string | null;
|
|
84
|
+
existingFieldIds?: string[]; // Lista de ids ya guardados para deduplicación
|
|
85
|
+
}): Promise<string | null> {
|
|
86
|
+
if (!isConfigured()) return null;
|
|
87
|
+
|
|
88
|
+
// Deduplicación: no insertar si ya existe en el contexto recuperado
|
|
89
|
+
if (field.existingFieldIds?.includes(field.field_identifier)) {
|
|
90
|
+
return null; // Ya catalogado, no duplicar
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`${getBase()}/portal/fields`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
'x-api-key': getKey()
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
project_id: getPid(),
|
|
102
|
+
page_id: field.page_id ?? null,
|
|
103
|
+
field_identifier: field.field_identifier.substring(0, 100),
|
|
104
|
+
field_type: field.field_type ?? 'text',
|
|
105
|
+
is_required: field.is_required ?? false,
|
|
106
|
+
validation_rule: field.validation_rule?.substring(0, 300) ?? null,
|
|
107
|
+
source: 'agent'
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = await res.json();
|
|
116
|
+
return data.id ?? null;
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
// Silencioso — fire-and-forget, 'terminated' es esperado al finalizar el proceso
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── 3. CONOCIMIENTO / DOCUMENTACIÓN ────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export type DocType = 'PROCESS' | 'VALIDATION' | 'NAVIGATION' | 'COMPONENT' | 'GENERAL';
|
|
126
|
+
|
|
127
|
+
export async function pushKnowledge(knowledge: {
|
|
128
|
+
doc_type: DocType;
|
|
129
|
+
title: string;
|
|
130
|
+
content: string;
|
|
131
|
+
}): Promise<string | null> {
|
|
132
|
+
if (!isConfigured()) return null;
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetch(`${getBase()}/portal/knowledge`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': 'application/json',
|
|
138
|
+
'x-api-key': getKey()
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
project_id: getPid(),
|
|
142
|
+
doc_type: knowledge.doc_type,
|
|
143
|
+
title: knowledge.title.substring(0, 150),
|
|
144
|
+
content: knowledge.content.substring(0, 500),
|
|
145
|
+
source: 'agent'
|
|
146
|
+
})
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
console.warn(`[CollectiveMemory] pushKnowledge HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const data = await res.json();
|
|
155
|
+
console.log(`\x1b[32m[CollectiveMemory] ✅ Conocimiento guardado: "${knowledge.title}"\x1b[0m`);
|
|
156
|
+
return data.id ?? null;
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
console.warn(`[CollectiveMemory] pushKnowledge error: ${err?.message}`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── HELPER: Obtener campos ya conocidos para deduplicación ─────────────────
|
|
164
|
+
|
|
165
|
+
export async function getKnownFieldIdentifiers(pathUrl: string): Promise<string[]> {
|
|
166
|
+
if (!isConfigured()) return [];
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(
|
|
169
|
+
`${getBase()}/portal/context?project_id=${getPid()}&path=${encodeURIComponent(pathUrl)}`,
|
|
170
|
+
{ headers: { 'x-api-key': getKey() } }
|
|
171
|
+
);
|
|
172
|
+
if (!res.ok) return [];
|
|
173
|
+
const data = await res.json();
|
|
174
|
+
return (data?.fields ?? []).map((f: any) => f.field_identifier ?? '').filter(Boolean);
|
|
175
|
+
} catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { select, isCancel, outro, spinner, text } from '@clack/prompts';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { ensureEnvInteractive, readExistingEnv } from './envSetup';
|
|
8
|
+
import { KnowledgeService } from './KnowledgeService';
|
|
9
|
+
import { ArcalityClient } from './arcalityClient';
|
|
10
|
+
|
|
11
|
+
type Choice = { title: string; value: string };
|
|
12
|
+
|
|
13
|
+
class UserCancelledError extends Error {
|
|
14
|
+
constructor(message = 'User cancelled the operation') {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'UserCancelledError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function walk(dir: string): string[] {
|
|
21
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
22
|
+
const files: string[] = [];
|
|
23
|
+
for (const e of entries) {
|
|
24
|
+
const full = path.join(dir, e.name);
|
|
25
|
+
if (e.isDirectory()) files.push(...walk(full));
|
|
26
|
+
else files.push(full);
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isPlaywrightTestFile(file: string): boolean {
|
|
32
|
+
return (
|
|
33
|
+
file.endsWith('.spec.ts') ||
|
|
34
|
+
file.endsWith('.spec.js') ||
|
|
35
|
+
file.endsWith('.test.ts') ||
|
|
36
|
+
file.endsWith('.test.js')
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getChoices(testDir: string): { label: string; value: string }[] {
|
|
41
|
+
if (!fs.existsSync(testDir)) return [];
|
|
42
|
+
|
|
43
|
+
const files = walk(testDir).filter(isPlaywrightTestFile).sort();
|
|
44
|
+
const rels = files.map((abs) => path.relative(process.cwd(), abs).split(path.sep).join('/'));
|
|
45
|
+
|
|
46
|
+
return [
|
|
47
|
+
...rels.map((r) => ({ label: r, value: r })),
|
|
48
|
+
{ label: '❌ Exit', value: '__EXIT__' },
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureInteractiveStdin(): void {
|
|
53
|
+
// If NO TTY, arrows will not work. Better to fail with a clear message.
|
|
54
|
+
if (!process.stdin.isTTY) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'The terminal is not interactive (stdin is not TTY). Run this command in a real terminal (not a pipe), and avoid ts-node-dev/watch.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function runPlaywright(selection: string): Promise<void> {
|
|
62
|
+
const isWin = process.platform === 'win32';
|
|
63
|
+
const pwBin = isWin
|
|
64
|
+
? path.join(process.cwd(), 'node_modules', '.bin', 'playwright.cmd')
|
|
65
|
+
: path.join(process.cwd(), 'node_modules', '.bin', 'playwright');
|
|
66
|
+
|
|
67
|
+
const baseArgs = ['test', '--headed', '--project=chromium'];
|
|
68
|
+
const args =
|
|
69
|
+
selection === '__ALL__'
|
|
70
|
+
? baseArgs
|
|
71
|
+
: [...baseArgs, selection];
|
|
72
|
+
|
|
73
|
+
const s = spinner();
|
|
74
|
+
s.start(`Running test: ${selection}`);
|
|
75
|
+
|
|
76
|
+
let triedNpx = false;
|
|
77
|
+
let lastError;
|
|
78
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
79
|
+
let bin = pwBin;
|
|
80
|
+
let binArgs = args;
|
|
81
|
+
let useShell = isWin;
|
|
82
|
+
if (triedNpx) {
|
|
83
|
+
bin = isWin ? 'npx.cmd' : 'npx';
|
|
84
|
+
binArgs = ['playwright', ...args];
|
|
85
|
+
useShell = isWin;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const spawnBin = (isWin && bin.includes(' ')) ? `"${bin}"` : bin;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await new Promise<void>((resolve, reject) => {
|
|
92
|
+
const child = spawn(spawnBin, binArgs, {
|
|
93
|
+
stdio: 'inherit',
|
|
94
|
+
shell: useShell,
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
env: process.env,
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
});
|
|
99
|
+
process.removeAllListeners('SIGINT');
|
|
100
|
+
const onSigint = () => {
|
|
101
|
+
child.kill('SIGINT');
|
|
102
|
+
reject(new UserCancelledError());
|
|
103
|
+
};
|
|
104
|
+
process.once('SIGINT', onSigint);
|
|
105
|
+
child.on('close', (code: any) => {
|
|
106
|
+
process.removeListener('SIGINT', onSigint);
|
|
107
|
+
if (code === 0) resolve();
|
|
108
|
+
else reject(new Error(`Arcality engine finished with code ${code}`));
|
|
109
|
+
});
|
|
110
|
+
child.on('error', (err: any) => {
|
|
111
|
+
process.removeListener('SIGINT', onSigint);
|
|
112
|
+
reject(err);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
s.message('Performing report rebranding...');
|
|
117
|
+
try {
|
|
118
|
+
const isWin = process.platform === 'win32';
|
|
119
|
+
const nodeCmd = isWin ? 'node.exe' : 'node';
|
|
120
|
+
const { spawnSync } = require('node:child_process');
|
|
121
|
+
spawnSync(nodeCmd, [path.join(process.cwd(), 'scripts', 'rebrand-report.mjs')], { stdio: 'ignore', windowsHide: true });
|
|
122
|
+
} catch (e) { /* ignore */ }
|
|
123
|
+
|
|
124
|
+
s.stop(chalk.green('✅ Test completed.'));
|
|
125
|
+
|
|
126
|
+
const reportPath = path.join(process.cwd(), 'tests-report', 'index.html');
|
|
127
|
+
if (fs.existsSync(reportPath)) {
|
|
128
|
+
if (isWin) {
|
|
129
|
+
spawn('cmd', ['/c', 'start', '""', `"${reportPath}"`], {
|
|
130
|
+
stdio: 'ignore',
|
|
131
|
+
shell: true,
|
|
132
|
+
detached: true,
|
|
133
|
+
windowsHide: true
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
137
|
+
spawn(openCmd, [reportPath], {
|
|
138
|
+
stdio: 'ignore',
|
|
139
|
+
shell: true,
|
|
140
|
+
detached: true
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
lastError = err;
|
|
147
|
+
if (!triedNpx) {
|
|
148
|
+
triedNpx = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
s.stop(chalk.red('❌ The test failed.'));
|
|
152
|
+
if (err instanceof Error) {
|
|
153
|
+
throw new Error(`Arcality Engine Error:\n${err.message}`);
|
|
154
|
+
} else {
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
throw lastError;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function promptAndRunPlaywrightTests(arcalityClient: ArcalityClient, knowledgeService: KnowledgeService): Promise<void> {
|
|
163
|
+
while (true) {
|
|
164
|
+
try {
|
|
165
|
+
await ensureEnvInteractive();
|
|
166
|
+
|
|
167
|
+
const testDir = path.join(process.cwd(), 'tests');
|
|
168
|
+
const options = getChoices(testDir);
|
|
169
|
+
|
|
170
|
+
if (options.length === 0) {
|
|
171
|
+
console.log('No tests found in:', testDir);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const selection = await select({
|
|
176
|
+
message: 'Select the test to run:',
|
|
177
|
+
options,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (isCancel(selection) || selection === '__EXIT__') {
|
|
181
|
+
outro(chalk.cyan('See you later!'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await runPlaywright(selection as string);
|
|
186
|
+
|
|
187
|
+
await text({
|
|
188
|
+
message: 'Press Enter to return to menu...',
|
|
189
|
+
placeholder: 'Enter'
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err instanceof UserCancelledError) {
|
|
194
|
+
outro(chalk.cyan('Operation cancelled.'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
console.error('Error:', err instanceof Error ? err.message : err);
|
|
198
|
+
await new Promise(res => setTimeout(res, 1000));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|