@diyor28/qa-core 0.1.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.
Files changed (67) hide show
  1. package/dist/agent/context/codecs/browser-state.codec.d.ts +33 -0
  2. package/dist/agent/context/codecs/browser-state.codec.d.ts.map +1 -0
  3. package/dist/agent/context/codecs/browser-state.codec.js +46 -0
  4. package/dist/agent/context/codecs/index.d.ts +3 -0
  5. package/dist/agent/context/codecs/index.d.ts.map +1 -0
  6. package/dist/agent/context/codecs/index.js +2 -0
  7. package/dist/agent/context/codecs/qa-scenario.codec.d.ts +21 -0
  8. package/dist/agent/context/codecs/qa-scenario.codec.d.ts.map +1 -0
  9. package/dist/agent/context/codecs/qa-scenario.codec.js +44 -0
  10. package/dist/agent/context/index.d.ts +4 -0
  11. package/dist/agent/context/index.d.ts.map +1 -0
  12. package/dist/agent/context/index.js +3 -0
  13. package/dist/agent/context/qa-context.service.d.ts +24 -0
  14. package/dist/agent/context/qa-context.service.d.ts.map +1 -0
  15. package/dist/agent/context/qa-context.service.js +53 -0
  16. package/dist/agent/context/qa-policy.d.ts +24 -0
  17. package/dist/agent/context/qa-policy.d.ts.map +1 -0
  18. package/dist/agent/context/qa-policy.js +47 -0
  19. package/dist/agent/loop.d.ts +28 -0
  20. package/dist/agent/loop.d.ts.map +1 -0
  21. package/dist/agent/loop.js +111 -0
  22. package/dist/agent/system-prompt.d.ts +2 -0
  23. package/dist/agent/system-prompt.d.ts.map +1 -0
  24. package/dist/agent/system-prompt.js +39 -0
  25. package/dist/agent/tools/index.d.ts +5 -0
  26. package/dist/agent/tools/index.d.ts.map +1 -0
  27. package/dist/agent/tools/index.js +152 -0
  28. package/dist/browser/controller.d.ts +82 -0
  29. package/dist/browser/controller.d.ts.map +1 -0
  30. package/dist/browser/controller.js +289 -0
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +7 -0
  34. package/dist/report/builder.d.ts +23 -0
  35. package/dist/report/builder.d.ts.map +1 -0
  36. package/dist/report/builder.js +69 -0
  37. package/dist/runner.d.ts +10 -0
  38. package/dist/runner.d.ts.map +1 -0
  39. package/dist/runner.js +143 -0
  40. package/dist/types.d.ts +69 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +1 -0
  43. package/dist/vision/analyzer.d.ts +8 -0
  44. package/dist/vision/analyzer.d.ts.map +1 -0
  45. package/dist/vision/analyzer.js +49 -0
  46. package/dist/vision/prompts.d.ts +2 -0
  47. package/dist/vision/prompts.d.ts.map +1 -0
  48. package/dist/vision/prompts.js +28 -0
  49. package/package.json +49 -0
  50. package/src/agent/context/codecs/browser-state.codec.ts +57 -0
  51. package/src/agent/context/codecs/index.ts +2 -0
  52. package/src/agent/context/codecs/qa-scenario.codec.ts +55 -0
  53. package/src/agent/context/index.ts +3 -0
  54. package/src/agent/context/qa-context.service.ts +90 -0
  55. package/src/agent/context/qa-policy.ts +54 -0
  56. package/src/agent/loop.ts +147 -0
  57. package/src/agent/system-prompt.ts +39 -0
  58. package/src/agent/tools/index.ts +162 -0
  59. package/src/browser/controller.ts +321 -0
  60. package/src/index.ts +19 -0
  61. package/src/report/builder.ts +94 -0
  62. package/src/runner.ts +166 -0
  63. package/src/types.ts +68 -0
  64. package/src/vision/analyzer.ts +61 -0
  65. package/src/vision/prompts.ts +28 -0
  66. package/tsconfig.json +20 -0
  67. package/tsconfig.tsbuildinfo +1 -0
package/dist/runner.js ADDED
@@ -0,0 +1,143 @@
1
+ import { BrowserController } from './browser/controller.js';
2
+ import { VisionAnalyzer } from './vision/analyzer.js';
3
+ import { AgentLoop } from './agent/loop.js';
4
+ import { ReportBuilder } from './report/builder.js';
5
+ import { readFileSync, readdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ export class QaRunner {
8
+ config;
9
+ browserController = null;
10
+ visionAnalyzer = null;
11
+ constructor(config) {
12
+ this.config = config;
13
+ }
14
+ async run() {
15
+ const startTime = Date.now();
16
+ try {
17
+ // Initialize components
18
+ this.emitProgress('step_started', { step: 'initialization', message: 'Initializing QA runner' });
19
+ this.browserController = new BrowserController({
20
+ headless: this.config.browser.headless,
21
+ viewport: this.config.browser.viewport,
22
+ enableTrace: this.config.browser.enableTrace,
23
+ traceDir: this.config.browser.traceDir,
24
+ });
25
+ this.visionAnalyzer = new VisionAnalyzer(this.config.models.geminiApiKey);
26
+ await this.browserController.launch();
27
+ this.emitProgress('step_completed', { step: 'initialization', message: 'Browser launched successfully' });
28
+ // Run agent loop
29
+ this.emitProgress('step_started', { step: 'execution', message: 'Starting test execution' });
30
+ const agentLoop = new AgentLoop({
31
+ cerebrasApiKey: this.config.models.cerebrasApiKey,
32
+ browserController: this.browserController,
33
+ visionAnalyzer: this.visionAnalyzer,
34
+ scenarioName: this.config.scenario.name,
35
+ scenarioContent: this.config.scenario.content,
36
+ baseUrl: this.config.baseUrl,
37
+ defectsToCheck: this.config.defectsToCheck || ['Visual overflow', 'Bad UX', 'Poor contrast'],
38
+ maxIterations: this.config.maxIterations,
39
+ onProgress: this.config.onProgress,
40
+ });
41
+ const agentResult = await agentLoop.run();
42
+ this.emitProgress('step_completed', {
43
+ step: 'execution',
44
+ message: 'Test execution completed',
45
+ findings: agentResult.findings.length,
46
+ });
47
+ // Collect artifacts
48
+ this.emitProgress('step_started', { step: 'artifacts', message: 'Collecting artifacts' });
49
+ const artifacts = [];
50
+ // Add all screenshots from browser controller
51
+ const screenshots = this.browserController.getAllScreenshots();
52
+ for (const screenshot of screenshots) {
53
+ artifacts.push({
54
+ type: 'screenshot',
55
+ name: screenshot.name,
56
+ data: screenshot.buffer,
57
+ contentType: 'image/png',
58
+ });
59
+ }
60
+ // Add trace file if enabled
61
+ if (this.config.browser.enableTrace && this.config.browser.traceDir) {
62
+ try {
63
+ const traceFiles = readdirSync(this.config.browser.traceDir).filter((f) => f.startsWith('trace-'));
64
+ if (traceFiles.length > 0) {
65
+ // Get the most recent trace file
66
+ const latestTrace = traceFiles.sort().pop();
67
+ const tracePath = join(this.config.browser.traceDir, latestTrace);
68
+ const traceBuffer = readFileSync(tracePath);
69
+ artifacts.push({
70
+ type: 'trace',
71
+ name: latestTrace,
72
+ data: traceBuffer,
73
+ contentType: 'application/zip',
74
+ });
75
+ }
76
+ }
77
+ catch (error) {
78
+ console.warn('[QaRunner] Failed to collect trace file:', error);
79
+ }
80
+ }
81
+ this.emitProgress('step_completed', {
82
+ step: 'artifacts',
83
+ message: 'Artifacts collected',
84
+ count: artifacts.length,
85
+ });
86
+ // Build report
87
+ const duration = Date.now() - startTime;
88
+ const report = ReportBuilder.buildReport({
89
+ findings: agentResult.findings,
90
+ artifacts,
91
+ metadata: {
92
+ scenarioName: this.config.scenario.name,
93
+ baseUrl: this.config.baseUrl,
94
+ viewport: this.config.browser.viewport,
95
+ timestamp: new Date().toISOString(),
96
+ },
97
+ duration,
98
+ summary: agentResult.summary,
99
+ });
100
+ return report;
101
+ }
102
+ catch (error) {
103
+ console.error('[QaRunner] Error during test execution:', error);
104
+ // Build error report
105
+ const duration = Date.now() - startTime;
106
+ return ReportBuilder.buildReport({
107
+ findings: [
108
+ {
109
+ severity: 'critical',
110
+ title: 'Test execution failed',
111
+ description: `Error during test execution: ${error instanceof Error ? error.message : String(error)}`,
112
+ reproSteps: ['Test runner encountered an error'],
113
+ category: 'functional',
114
+ },
115
+ ],
116
+ artifacts: [],
117
+ metadata: {
118
+ scenarioName: this.config.scenario.name,
119
+ baseUrl: this.config.baseUrl,
120
+ viewport: this.config.browser.viewport,
121
+ timestamp: new Date().toISOString(),
122
+ },
123
+ duration,
124
+ summary: `Test execution failed: ${error instanceof Error ? error.message : String(error)}`,
125
+ });
126
+ }
127
+ finally {
128
+ // Always close browser
129
+ if (this.browserController) {
130
+ await this.browserController.close();
131
+ }
132
+ }
133
+ }
134
+ emitProgress(type, data) {
135
+ if (this.config.onProgress) {
136
+ this.config.onProgress({
137
+ type,
138
+ data,
139
+ timestamp: Date.now(),
140
+ });
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,69 @@
1
+ export interface QaScenario {
2
+ name: string;
3
+ content: string;
4
+ }
5
+ export interface QaFinding {
6
+ severity: 'low' | 'medium' | 'high' | 'critical';
7
+ title: string;
8
+ description: string;
9
+ reproSteps: string[];
10
+ expected?: string;
11
+ actual?: string;
12
+ screenshotName?: string;
13
+ category: 'functional' | 'ux' | 'ui' | 'accessibility';
14
+ }
15
+ export interface QaArtifact {
16
+ type: 'screenshot' | 'trace';
17
+ name: string;
18
+ data: Buffer;
19
+ contentType: string;
20
+ }
21
+ export interface QaReport {
22
+ summary: string;
23
+ status: 'passed' | 'failed' | 'issues_found';
24
+ duration: number;
25
+ findings: QaFinding[];
26
+ artifacts: QaArtifact[];
27
+ metadata: {
28
+ scenarioName: string;
29
+ baseUrl: string;
30
+ viewport: {
31
+ width: number;
32
+ height: number;
33
+ };
34
+ timestamp: string;
35
+ };
36
+ }
37
+ export interface QaProgressEvent {
38
+ type: 'step_started' | 'step_completed' | 'screenshot_captured' | 'finding_discovered' | 'context_usage';
39
+ data: Record<string, unknown>;
40
+ timestamp: number;
41
+ }
42
+ export interface ScreenshotEntry {
43
+ timestamp: number;
44
+ name: string;
45
+ buffer: Buffer;
46
+ context: string;
47
+ url: string;
48
+ }
49
+ export interface QaRunnerConfig {
50
+ browser: {
51
+ headless?: boolean;
52
+ viewport: {
53
+ width: number;
54
+ height: number;
55
+ };
56
+ enableTrace?: boolean;
57
+ traceDir?: string;
58
+ };
59
+ models: {
60
+ cerebrasApiKey: string;
61
+ geminiApiKey: string;
62
+ };
63
+ baseUrl: string;
64
+ scenario: QaScenario;
65
+ defectsToCheck?: string[];
66
+ maxIterations?: number;
67
+ onProgress?: (event: QaProgressEvent) => void;
68
+ }
69
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,eAAe,CAAC;CACxD;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,cAAc,CAAC;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,QAAQ,EAAE;QACR,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5C,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,cAAc,GAAG,gBAAgB,GAAG,qBAAqB,GAAG,oBAAoB,GAAG,eAAe,CAAC;IACzG,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE;QACP,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,MAAM,EAAE;QACN,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,UAAU,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC/C"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { QaFinding } from '../types.js';
2
+ export declare class VisionAnalyzer {
3
+ private genAI;
4
+ private model;
5
+ constructor(apiKey: string);
6
+ analyzeScreenshot(screenshot: Buffer, context: string, url: string): Promise<QaFinding[]>;
7
+ }
8
+ //# sourceMappingURL=analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/vision/analyzer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,KAAK,CAAM;gBAEP,MAAM,EAAE,MAAM;IAOpB,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,SAAS,EAAE,CAAC;CAyCxB"}
@@ -0,0 +1,49 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+ import { buildVisionPrompt } from './prompts.js';
3
+ export class VisionAnalyzer {
4
+ genAI;
5
+ model;
6
+ constructor(apiKey) {
7
+ this.genAI = new GoogleGenerativeAI(apiKey);
8
+ this.model = this.genAI.getGenerativeModel({
9
+ model: 'gemini-3-flash-preview',
10
+ });
11
+ }
12
+ async analyzeScreenshot(screenshot, context, url) {
13
+ try {
14
+ const base64Image = screenshot.toString('base64');
15
+ const prompt = buildVisionPrompt(context, url);
16
+ const result = await this.model.generateContent([
17
+ {
18
+ inlineData: {
19
+ mimeType: 'image/png',
20
+ data: base64Image,
21
+ },
22
+ },
23
+ { text: prompt },
24
+ ]);
25
+ const response = await result.response;
26
+ const text = response.text();
27
+ // Parse JSON response
28
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
29
+ if (!jsonMatch) {
30
+ console.warn('[VisionAnalyzer] No JSON found in response:', text);
31
+ return [];
32
+ }
33
+ const parsed = JSON.parse(jsonMatch[0]);
34
+ const findings = parsed.findings || [];
35
+ // Convert vision findings to QaFinding format
36
+ return findings.map((finding) => ({
37
+ severity: finding.severity || 'medium',
38
+ title: finding.title,
39
+ description: `${finding.description}\n\nSuggestion: ${finding.suggestion || 'N/A'}`,
40
+ reproSteps: [`Screenshot captured at: ${url}`, `Context: ${context}`],
41
+ category: finding.category,
42
+ }));
43
+ }
44
+ catch (error) {
45
+ console.error('[VisionAnalyzer] Error analyzing screenshot:', error);
46
+ return [];
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,2 @@
1
+ export declare function buildVisionPrompt(context: string, url: string): string;
2
+ //# sourceMappingURL=prompts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/vision/prompts.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CA2BtE"}
@@ -0,0 +1,28 @@
1
+ export function buildVisionPrompt(context, url) {
2
+ return `You are a ruthless UX/UI critic analyzing a screenshot of a web application.
3
+
4
+ Context: ${context}
5
+ URL: ${url}
6
+
7
+ Analyze this screenshot for:
8
+ - Visual overflow (content cut off, horizontal scroll)
9
+ - Poor color contrast (WCAG AA violations)
10
+ - Bad UX (confusing navigation, hidden CTAs, poor hierarchy)
11
+ - Accessibility issues (missing labels, poor focus indicators)
12
+
13
+ Return findings as JSON array with structure:
14
+ {
15
+ "findings": [
16
+ {
17
+ "category": "ux" | "ui" | "accessibility",
18
+ "severity": "low" | "medium" | "high",
19
+ "title": "Short title",
20
+ "description": "Detailed description",
21
+ "suggestion": "How to fix"
22
+ }
23
+ ]
24
+ }
25
+
26
+ Be concise and specific. Only report actual issues you can clearly see in the screenshot.
27
+ If everything looks good, return an empty findings array.`;
28
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@diyor28/qa-core",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Core library for AI-powered browser testing with Playwright, GLM-4.7, and Gemini vision analysis",
6
+ "keywords": [
7
+ "qa",
8
+ "testing",
9
+ "playwright",
10
+ "ai",
11
+ "browser-automation",
12
+ "e2e"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/iota-uz/foundry.git",
17
+ "directory": "packages/qa-core"
18
+ },
19
+ "license": "MIT",
20
+ "type": "module",
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "dependencies": {
33
+ "@ai-sdk/openai": "^1.0.10",
34
+ "@google/generative-ai": "^0.21.0",
35
+ "ai": "^4.0.61",
36
+ "playwright": "^1.49.0",
37
+ "zod": "^3.24.1",
38
+ "@diyor28/context": "1.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.11.5",
42
+ "typescript": "^5.3.3"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc",
46
+ "dev": "tsc --watch",
47
+ "typecheck": "tsc --noEmit"
48
+ }
49
+ }
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ import type { BlockCodec, RenderedContent, ContextBlock } from '@diyor28/context';
3
+ import { defaultHash, sortObjectKeys } from '@diyor28/context';
4
+
5
+ export const BrowserStatePayloadSchema = z.object({
6
+ currentUrl: z.string(),
7
+ viewport: z.object({
8
+ width: z.number(),
9
+ height: z.number(),
10
+ }),
11
+ screenshotCount: z.number(),
12
+ });
13
+
14
+ export type BrowserStatePayload = z.infer<typeof BrowserStatePayloadSchema>;
15
+
16
+ export const BrowserStateCodec: BlockCodec<BrowserStatePayload> = {
17
+ codecId: 'qa:browser-state',
18
+ version: '1.0.0',
19
+ payloadSchema: BrowserStatePayloadSchema,
20
+
21
+ canonicalize(payload: BrowserStatePayload): unknown {
22
+ return sortObjectKeys(payload);
23
+ },
24
+
25
+ hash(canonicalized: unknown): string {
26
+ return defaultHash(canonicalized);
27
+ },
28
+
29
+ render(block: ContextBlock<BrowserStatePayload>): RenderedContent {
30
+ const { currentUrl, viewport, screenshotCount } = block.payload;
31
+
32
+ const content = `## Browser State
33
+
34
+ Current URL: ${currentUrl}
35
+ Viewport: ${viewport.width}x${viewport.height}
36
+ Screenshots captured: ${screenshotCount}`;
37
+
38
+ return {
39
+ anthropic: {
40
+ role: 'user',
41
+ content: [
42
+ {
43
+ type: 'text',
44
+ text: content,
45
+ cache_control: { type: 'ephemeral' }, // State blocks use ephemeral cache
46
+ },
47
+ ],
48
+ },
49
+ openai: { role: 'user', content },
50
+ gemini: { role: 'user', parts: [{ text: content }] },
51
+ };
52
+ },
53
+
54
+ validate(payload: unknown): BrowserStatePayload {
55
+ return BrowserStatePayloadSchema.parse(payload);
56
+ },
57
+ };
@@ -0,0 +1,2 @@
1
+ export { QaScenarioCodec, QaScenarioPayloadSchema, type QaScenarioPayload } from './qa-scenario.codec.js';
2
+ export { BrowserStateCodec, BrowserStatePayloadSchema, type BrowserStatePayload } from './browser-state.codec.js';
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import type { BlockCodec, RenderedContent, ContextBlock } from '@diyor28/context';
3
+ import { defaultHash, sortObjectKeys } from '@diyor28/context';
4
+
5
+ export const QaScenarioPayloadSchema = z.object({
6
+ scenarioName: z.string(),
7
+ instructions: z.string(),
8
+ baseUrl: z.string().url(),
9
+ defectsToCheck: z.array(z.string()),
10
+ });
11
+
12
+ export type QaScenarioPayload = z.infer<typeof QaScenarioPayloadSchema>;
13
+
14
+ export const QaScenarioCodec: BlockCodec<QaScenarioPayload> = {
15
+ codecId: 'qa:scenario',
16
+ version: '1.0.0',
17
+ payloadSchema: QaScenarioPayloadSchema,
18
+
19
+ canonicalize(payload: QaScenarioPayload): unknown {
20
+ return sortObjectKeys({
21
+ scenarioName: payload.scenarioName.trim(),
22
+ instructions: payload.instructions.trim(),
23
+ baseUrl: payload.baseUrl,
24
+ defectsToCheck: [...payload.defectsToCheck].sort(),
25
+ });
26
+ },
27
+
28
+ hash(canonicalized: unknown): string {
29
+ return defaultHash(canonicalized);
30
+ },
31
+
32
+ render(block: ContextBlock<QaScenarioPayload>): RenderedContent {
33
+ const { scenarioName, instructions, baseUrl, defectsToCheck } = block.payload;
34
+
35
+ const content = `# Test Scenario: ${scenarioName}
36
+
37
+ **Target URL:** ${baseUrl}
38
+
39
+ ## Instructions
40
+ ${instructions}
41
+
42
+ ## Defects to Check
43
+ ${defectsToCheck.map((d) => `- ${d}`).join('\n')}`;
44
+
45
+ return {
46
+ anthropic: { role: 'user', content },
47
+ openai: { role: 'user', content },
48
+ gemini: { role: 'user', parts: [{ text: content }] },
49
+ };
50
+ },
51
+
52
+ validate(payload: unknown): QaScenarioPayload {
53
+ return QaScenarioPayloadSchema.parse(payload);
54
+ },
55
+ };
@@ -0,0 +1,3 @@
1
+ export { QaContextService, type BuildQaContextOptions } from './qa-context.service.js';
2
+ export { QA_POLICY } from './qa-policy.js';
3
+ export * from './codecs/index.js';
@@ -0,0 +1,90 @@
1
+ import {
2
+ ContextBuilder,
3
+ type ContextPolicy,
4
+ type OpenAICompiledContext,
5
+ type BlockCodec,
6
+ compileOpenAIContext,
7
+ OpenAITokenEstimator,
8
+ ConversationHistoryCodec,
9
+ type ConversationMessage,
10
+ SystemRulesCodec,
11
+ } from '@diyor28/context';
12
+ import { QaScenarioCodec, BrowserStateCodec } from './codecs/index.js';
13
+ import { QA_POLICY } from './qa-policy.js';
14
+
15
+ export interface BuildQaContextOptions {
16
+ scenarioName: string;
17
+ scenarioInstructions: string;
18
+ baseUrl: string;
19
+ defectsToCheck: string[];
20
+ browserState: {
21
+ currentUrl: string;
22
+ viewport: { width: number; height: number };
23
+ screenshotCount: number;
24
+ };
25
+ conversationHistory: ConversationMessage[];
26
+ systemPrompt: string;
27
+ }
28
+
29
+ export class QaContextService {
30
+ private readonly tokenEstimator: OpenAITokenEstimator;
31
+ private readonly codecRegistry: Map<string, BlockCodec<unknown>>;
32
+
33
+ constructor() {
34
+ // OpenAI token estimator (compatible with Cerebras/GLM-4.7)
35
+ this.tokenEstimator = new OpenAITokenEstimator('gpt-4');
36
+
37
+ // Build codec registry
38
+ this.codecRegistry = new Map<string, BlockCodec<unknown>>([
39
+ [SystemRulesCodec.codecId, SystemRulesCodec as BlockCodec<unknown>],
40
+ [QaScenarioCodec.codecId, QaScenarioCodec as BlockCodec<unknown>],
41
+ [BrowserStateCodec.codecId, BrowserStateCodec as BlockCodec<unknown>],
42
+ [ConversationHistoryCodec.codecId, ConversationHistoryCodec as BlockCodec<unknown>],
43
+ ]);
44
+ }
45
+
46
+ async buildContext(options: BuildQaContextOptions): Promise<OpenAICompiledContext> {
47
+ const builder = new ContextBuilder();
48
+
49
+ // System prompt (pinned - always at top)
50
+ builder.system({
51
+ text: options.systemPrompt,
52
+ cacheable: false, // OpenAI doesn't have prompt caching yet
53
+ });
54
+
55
+ // QA scenario (reference - test instructions)
56
+ builder.reference(QaScenarioCodec, {
57
+ scenarioName: options.scenarioName,
58
+ instructions: options.scenarioInstructions,
59
+ baseUrl: options.baseUrl,
60
+ defectsToCheck: options.defectsToCheck,
61
+ });
62
+
63
+ // Browser state (state - current browser info)
64
+ builder.state(BrowserStateCodec, options.browserState);
65
+
66
+ // Conversation history (if any)
67
+ if (options.conversationHistory.length > 0) {
68
+ builder.history(options.conversationHistory);
69
+ }
70
+
71
+ // Get graph and create view
72
+ const graph = builder.getGraph();
73
+ const maxTokens = QA_POLICY.contextWindow - QA_POLICY.completionReserve;
74
+
75
+ const view = await graph.createView({
76
+ tokenEstimator: this.tokenEstimator,
77
+ maxTokens,
78
+ });
79
+
80
+ // Compile to OpenAI format
81
+ const compiled = compileOpenAIContext([...view.blocks], QA_POLICY, {
82
+ codecRegistry: this.codecRegistry,
83
+ });
84
+
85
+ // Log context usage (for observability)
86
+ console.log(`[QA Context] Tokens: ${compiled.estimatedTokens}, Blocks: ${view.blocks.length}`);
87
+
88
+ return compiled;
89
+ }
90
+ }
@@ -0,0 +1,54 @@
1
+ import type { ContextPolicy } from '@diyor28/context';
2
+
3
+ /**
4
+ * QA Context Policy
5
+ *
6
+ * Defines context management strategy for QA agent execution.
7
+ * Optimized for browser automation with tool outputs and screenshot management.
8
+ *
9
+ * ## Context Structure
10
+ *
11
+ * 1. System prompt (pinned)
12
+ * 2. QA scenario (reference - test instructions)
13
+ * 3. Browser state (state - current URL, viewport)
14
+ * 4. Tool outputs (tool_output - navigation, clicks, screenshots)
15
+ * 5. Conversation history (history - agent reasoning)
16
+ *
17
+ * ## Compaction Triggers
18
+ *
19
+ * When context exceeds available tokens:
20
+ * - Old tool outputs are pruned (keep last 5)
21
+ * - History is summarized
22
+ * - State blocks are never truncated
23
+ */
24
+ export const QA_POLICY: ContextPolicy = {
25
+ provider: 'openai', // Cerebras is OpenAI-compatible
26
+ modelId: 'glm-4.7', // GLM-4.7 via Cerebras
27
+
28
+ contextWindow: 200_000, // GLM-4.7 context window
29
+ completionReserve: 8_000, // Reserve for agent response
30
+
31
+ overflowStrategy: 'compact', // Auto-compact old tool outputs
32
+
33
+ kindPriorities: [
34
+ { kind: 'pinned', minTokens: 500, maxTokens: 2000, truncatable: false },
35
+ { kind: 'reference', minTokens: 500, maxTokens: 3000, truncatable: false },
36
+ { kind: 'state', minTokens: 200, maxTokens: 1000, truncatable: false },
37
+ { kind: 'tool_output', minTokens: 1000, maxTokens: 50000, truncatable: true },
38
+ { kind: 'history', minTokens: 2000, maxTokens: 100000, truncatable: true },
39
+ { kind: 'turn', minTokens: 500, maxTokens: 5000, truncatable: false },
40
+ ],
41
+
42
+ compaction: {
43
+ pruneToolOutputs: true,
44
+ maxToolOutputAge: 3600, // 1 hour
45
+ maxToolOutputsPerKind: 5, // Keep last 5 tool outputs
46
+ summarizeHistory: true,
47
+ maxHistoryMessages: 20, // Keep last 20 turns
48
+ },
49
+
50
+ sensitivity: {
51
+ maxSensitivity: 'public',
52
+ redactRestricted: true,
53
+ },
54
+ };