qa360 2.2.0 → 2.2.1

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.
Binary file
Binary file
Binary file
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { existsSync, readFileSync } from 'fs';
11
- import { join, resolve } from 'path';
11
+ import { join, resolve, dirname } from 'path';
12
12
  import chalk from 'chalk';
13
13
  import { load } from 'js-yaml';
14
14
  import { Phase3Runner, PackLoaderV2, type Phase3RunResult, type GateResult, type PackConfigV1, type PackConfigV2 } from 'qa360-core';
@@ -183,13 +183,15 @@ export async function runCommand(
183
183
 
184
184
  // Step 4: Create and run Phase3Runner
185
185
  const workingDir = process.cwd();
186
- const outputDir = options.output
187
- ? resolve(options.output)
186
+ const packDir = dirname(packPath); // Directory containing the pack file
187
+ const outputDir = options.output
188
+ ? resolve(options.output)
188
189
  : join(workingDir, '.qa360', 'runs');
189
190
 
190
191
  const runner = new Phase3Runner({
191
192
  workingDir,
192
193
  pack,
194
+ packDir, // Pass pack directory for resolving fixtures
193
195
  outputDir,
194
196
  });
195
197
 
package/core/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360-core",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "QA360 Core Engine - Proof generation, signatures, and evidence vault",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -84,9 +84,20 @@ export class FixtureLoader {
84
84
  );
85
85
  }
86
86
 
87
- // Use the filename (without extension) as the fixture name key
87
+ // Merge fixture data into registry (keys become directly accessible)
88
+ // This allows $fixtures.environments.production.baseUrl instead of
89
+ // $fixtures.fixtures.environments.production.baseUrl
88
90
  const fixtureName = this.getFixtureName(fixturePath);
89
- this.registry[fixtureName] = fixtureData;
91
+ this.registry[fixtureName] = fixtureData; // Keep filename reference for debug
92
+
93
+ // Also merge top-level keys for direct access
94
+ for (const [key, value] of Object.entries(fixtureData)) {
95
+ // Skip if key already exists (first file wins)
96
+ // We need to cast because FixtureValue is broader than FixtureFile
97
+ if (!(key in this.registry)) {
98
+ (this.registry as Record<string, unknown>)[key] = value;
99
+ }
100
+ }
90
101
 
91
102
  return fixtureData;
92
103
  } catch (error) {
@@ -569,7 +569,7 @@ describe('PackValidatorV2', () => {
569
569
  expect(result.valid).toBe(true);
570
570
  });
571
571
 
572
- it('should reject fixture path with parent directory reference', async () => {
572
+ it('should warn about fixture path with parent directory reference', async () => {
573
573
  const pack: PackConfigV2 = {
574
574
  version: 2,
575
575
  name: 'test-pack',
@@ -577,8 +577,8 @@ describe('PackValidatorV2', () => {
577
577
  gates: { smoke: { adapter: 'playwright-api' } }
578
578
  };
579
579
  const result = await validator.validate(pack);
580
- expect(result.valid).toBe(false);
581
- expect(result.errors.some(e => e.code === 'QP2V038')).toBe(true);
580
+ expect(result.valid).toBe(true); // Changed from false to true - now just a warning
581
+ expect(result.warnings.some(e => e.code === 'QP2V038')).toBe(true); // Changed from errors to warnings
582
582
  });
583
583
 
584
584
  it('should warn about unknown file extension', async () => {
@@ -186,13 +186,14 @@ export class PackValidatorV2 {
186
186
  continue;
187
187
  }
188
188
 
189
- // Check for invalid patterns
189
+ // Check for patterns that might cause issues
190
190
  if (fixturePath.includes('..')) {
191
- errors.push({
191
+ // Changed from error to warning - parent directory references are sometimes needed
192
+ warnings.push({
192
193
  code: 'QP2V038',
193
- message: 'Fixture path cannot contain parent directory reference ".."',
194
+ message: `Fixture path contains parent directory reference "..": ${fixturePath}`,
194
195
  path: `fixtures.${fixturePath}`,
195
- suggestion: 'Use relative patterns without ..'
196
+ suggestion: 'Consider using absolute paths or placing fixtures in the same directory as the pack'
196
197
  });
197
198
  }
198
199
 
@@ -384,7 +385,11 @@ export class PackValidatorV2 {
384
385
  'semgrep-sast',
385
386
  'zap-dast',
386
387
  'gitleaks-secrets',
387
- 'osv-deps'
388
+ 'osv-deps',
389
+ // v2.2.0 Unit Test Adapters
390
+ 'vitest',
391
+ 'jest',
392
+ 'pytest'
388
393
  ];
389
394
 
390
395
  for (const [gateName, gate] of Object.entries(gates)) {
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { existsSync, writeFileSync, mkdirSync } from 'fs';
8
- import { join } from 'path';
8
+ import { join, resolve } from 'path';
9
9
  import { createHash } from 'crypto';
10
10
  import chalk from 'chalk';
11
11
  import { PackConfigV1 } from '../types/pack-v1.js';
@@ -15,6 +15,14 @@ import { PlaywrightNativeApiAdapter } from '../adapters/playwright-native-api.js
15
15
  import { PlaywrightUiAdapter } from '../adapters/playwright-ui.js';
16
16
  import { K6PerfAdapter } from '../adapters/k6-perf.js';
17
17
  import { SemgrepSastAdapter } from '../adapters/semgrep-sast.js';
18
+ // v2.2.0 Unit Test Adapters
19
+ import { VitestAdapter } from '../adapters/vitest-adapter.js';
20
+ import { JestAdapter } from '../adapters/jest-adapter.js';
21
+ import { PytestAdapter } from '../adapters/pytest-adapter.js';
22
+ // v2.2.0 Data Fixtures
23
+ import { FixtureLoader } from '../fixtures/loader.js';
24
+ import { FixtureResolver } from '../fixtures/resolver.js';
25
+ import { PageObjectLoader } from '../pom/loader.js';
18
26
  import { SecurityRedactor } from '../security/redactor.js';
19
27
  import { initializeKeys, sign, KeyPair } from '../proof/signer.js';
20
28
  import { canonicalize } from '../proof/canonicalize.js';
@@ -35,6 +43,7 @@ export type PackConfig = PackConfigV1 | PackConfigV2;
35
43
  export interface Phase3RunnerOptions {
36
44
  workingDir: string;
37
45
  pack: PackConfig;
46
+ packDir?: string; // Directory containing the pack file (for resolving fixtures)
38
47
  outputDir?: string;
39
48
  /** Enable flakiness detection (runs tests N times consecutively) */
40
49
  flakyDetect?: boolean;
@@ -80,6 +89,7 @@ export interface Phase3RunResult {
80
89
  export class Phase3Runner {
81
90
  private workingDir: string;
82
91
  private pack: PackConfig;
92
+ private packDir: string; // Directory containing the pack file
83
93
  private outputDir: string;
84
94
  private redactor: SecurityRedactor;
85
95
  private hooksRunner: HooksRunner;
@@ -90,10 +100,15 @@ export class Phase3Runner {
90
100
  private flakyDetect: boolean;
91
101
  private flakyRuns: number;
92
102
  private flakinessDetector: FlakinessDetector;
103
+ // v2.2.0 Data Fixtures & POM
104
+ private fixtureLoader?: FixtureLoader;
105
+ private fixtureResolver?: FixtureResolver;
106
+ private pageObjectLoader?: PageObjectLoader;
93
107
 
94
108
  constructor(options: Phase3RunnerOptions) {
95
109
  this.workingDir = options.workingDir;
96
110
  this.pack = options.pack;
111
+ this.packDir = options.packDir || options.workingDir;
97
112
  this.outputDir = options.outputDir || join(this.workingDir, '.qa360', 'runs');
98
113
  this.redactor = SecurityRedactor.forLogs();
99
114
  this.authManager = new AuthManager();
@@ -121,6 +136,101 @@ export class Phase3Runner {
121
136
  if (this.isPackV2(options.pack) && options.pack.auth?.profiles) {
122
137
  this.registerAuthProfiles(options.pack.auth.profiles);
123
138
  }
139
+
140
+ // Load data fixtures from pack v2
141
+ if (this.isPackV2(options.pack) && options.pack.fixtures && options.pack.fixtures.length > 0) {
142
+ this.initializeFixtures(options.pack.fixtures);
143
+ }
144
+
145
+ // Initialize Page Object Model from pack v2
146
+ if (this.isPackV2(options.pack) && options.pack.pageObjects) {
147
+ this.initializePageObjects(options.pack.pageObjects);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Initialize Page Object Model from the pack configuration
153
+ */
154
+ private initializePageObjects(config: { directory?: string; pattern?: string; baseUrl?: string }): void {
155
+ try {
156
+ const pomDir = config.directory ? resolve(this.packDir, config.directory) : this.packDir;
157
+ this.pageObjectLoader = new PageObjectLoader({ cwd: pomDir });
158
+ console.log(chalk.gray(` 📄 POM loader initialized for: ${pomDir}`));
159
+ } catch (error) {
160
+ console.log(chalk.yellow(`⚠️ Failed to initialize POM loader: ${error instanceof Error ? error.message : 'Unknown error'}`));
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Load Page Objects before execution
166
+ */
167
+ private async loadPageObjects(): Promise<void> {
168
+ if (!this.pageObjectLoader || !this.isPackV2(this.pack) || !this.pack.pageObjects) {
169
+ return;
170
+ }
171
+
172
+ try {
173
+ const pattern = this.pack.pageObjects.pattern || '**/*.page.{ts,js}';
174
+ const result = await this.pageObjectLoader.loadFromDirectory(this.packDir, { pattern });
175
+ console.log(chalk.green(` ✅ Page Objects loaded: ${result.pagesCount} pages`));
176
+ } catch (error) {
177
+ console.log(chalk.yellow(`⚠️ Failed to load page objects: ${error instanceof Error ? error.message : 'Unknown error'}`));
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Initialize data fixtures from the pack configuration
183
+ */
184
+ private initializeFixtures(fixturePaths: string[]): void {
185
+ try {
186
+ this.fixtureLoader = new FixtureLoader(this.packDir);
187
+ // Load fixtures synchronously - they're already validated
188
+ // We'll load them before execution starts
189
+ this.fixtureResolver = new FixtureResolver(this.fixtureLoader, this.packDir);
190
+ } catch (error) {
191
+ console.log(chalk.yellow(`⚠️ Failed to initialize fixtures: ${error instanceof Error ? error.message : 'Unknown error'}`));
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Load fixtures before execution
197
+ */
198
+ private async loadFixtures(): Promise<void> {
199
+ if (!this.fixtureLoader || !this.isPackV2(this.pack) || !this.pack.fixtures) {
200
+ return;
201
+ }
202
+
203
+ try {
204
+ await this.fixtureLoader.load(this.pack.fixtures);
205
+ console.log(chalk.green(` ✅ Fixtures loaded: ${this.fixtureLoader.getFixtureNames().join(', ')}`));
206
+ } catch (error) {
207
+ console.log(chalk.yellow(`⚠️ Failed to load fixtures: ${error instanceof Error ? error.message : 'Unknown error'}`));
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Resolve fixture references in a configuration object
213
+ */
214
+ private resolveFixturesInConfig(config: any): any {
215
+ if (!this.fixtureResolver || !config) {
216
+ return config;
217
+ }
218
+
219
+ try {
220
+ const result = this.fixtureResolver.resolve(config, {
221
+ baseDir: this.workingDir,
222
+ keepUnresolved: false
223
+ });
224
+
225
+ if (result.hasFixtures) {
226
+ console.log(chalk.gray(` 🔧 Fixtures resolved in config`));
227
+ }
228
+
229
+ return result.value;
230
+ } catch (error) {
231
+ console.log(chalk.yellow(`⚠️ Failed to resolve fixtures: ${error instanceof Error ? error.message : 'Unknown error'}`));
232
+ return config;
233
+ }
124
234
  }
125
235
 
126
236
  /**
@@ -269,11 +379,17 @@ export class Phase3Runner {
269
379
 
270
380
  console.log(chalk.bold.blue(`\n🚀 QA360 Phase 3 Runner - ${this.pack.name}`));
271
381
  console.log(chalk.gray(`Gates: ${gatesArray.join(', ')}`));
272
-
382
+
273
383
  try {
274
384
  // Ensure output directory exists
275
385
  this.ensureOutputDir();
276
-
386
+
387
+ // Load data fixtures if configured
388
+ await this.loadFixtures();
389
+
390
+ // Load page objects if configured
391
+ await this.loadPageObjects();
392
+
277
393
  // Initialize cryptographic keys
278
394
  console.log(chalk.blue('\n🔑 Initializing Ed25519 keys...'));
279
395
  this.keyPair = await initializeKeys();
@@ -548,6 +664,9 @@ export class Phase3Runner {
548
664
  throw new Error(`Gate '${gateName}' must specify an adapter`);
549
665
  }
550
666
 
667
+ // Resolve fixtures in gate configuration
668
+ const resolvedGateConfig = this.resolveFixturesInConfig(gateConfig);
669
+
551
670
  // Get auth credentials for this gate
552
671
  const credentials = await this.getCredentialsForGate(gateName);
553
672
 
@@ -555,21 +674,34 @@ export class Phase3Runner {
555
674
  let result: GateResult;
556
675
  switch (adapterType) {
557
676
  case 'playwright-api':
558
- result = await this.executePlaywrightApiGate(gateName, gateConfig, credentials);
677
+ result = await this.executePlaywrightApiGate(gateName, resolvedGateConfig, credentials);
559
678
  break;
560
679
 
561
680
  case 'playwright-ui':
562
- result = await this.executePlaywrightUiGate(gateName, gateConfig, credentials);
681
+ result = await this.executePlaywrightUiGate(gateName, resolvedGateConfig, credentials);
563
682
  break;
564
683
 
565
684
  case 'k6':
566
685
  case 'k6-perf':
567
- result = await this.executeK6PerfGate(gateName, gateConfig);
686
+ result = await this.executeK6PerfGate(gateName, resolvedGateConfig);
568
687
  break;
569
688
 
570
689
  case 'semgrep':
571
690
  case 'sast':
572
- result = await this.executeSemgrepSastGate(gateName, gateConfig);
691
+ result = await this.executeSemgrepSastGate(gateName, resolvedGateConfig);
692
+ break;
693
+
694
+ // v2.2.0 Unit Test Adapters
695
+ case 'vitest':
696
+ result = await this.executeVitestGate(gateName, resolvedGateConfig);
697
+ break;
698
+
699
+ case 'jest':
700
+ result = await this.executeJestGate(gateName, resolvedGateConfig);
701
+ break;
702
+
703
+ case 'pytest':
704
+ result = await this.executePytestGate(gateName, resolvedGateConfig);
573
705
  break;
574
706
 
575
707
  default:
@@ -731,6 +863,174 @@ export class Phase3Runner {
731
863
  };
732
864
  }
733
865
 
866
+ /**
867
+ * Execute Vitest unit tests gate (v2.2.0)
868
+ */
869
+ private async executeVitestGate(gateName: string, gateConfig: any): Promise<GateResult> {
870
+ const adapter = new VitestAdapter(this.workingDir);
871
+
872
+ const config: any = {
873
+ cwd: this.workingDir,
874
+ ...gateConfig.config
875
+ };
876
+
877
+ const result = await adapter.execute(config);
878
+
879
+ return {
880
+ gate: gateName,
881
+ success: result.success,
882
+ duration: result.duration,
883
+ adapter: 'vitest',
884
+ results: {
885
+ total: result.total,
886
+ passed: result.passed,
887
+ failed: result.failed,
888
+ skipped: result.skipped,
889
+ tests: result.tests.map(t => ({
890
+ name: t.name,
891
+ status: t.status,
892
+ duration: t.duration,
893
+ error: t.error
894
+ }))
895
+ },
896
+ junit: this.buildJunitFromVitest(result)
897
+ };
898
+ }
899
+
900
+ /**
901
+ * Execute Jest unit tests gate (v2.2.0)
902
+ */
903
+ private async executeJestGate(gateName: string, gateConfig: any): Promise<GateResult> {
904
+ const adapter = new JestAdapter(this.workingDir);
905
+
906
+ const config: any = {
907
+ cwd: this.workingDir,
908
+ ...gateConfig.config
909
+ };
910
+
911
+ const result = await adapter.execute(config);
912
+
913
+ return {
914
+ gate: gateName,
915
+ success: result.success,
916
+ duration: result.duration,
917
+ adapter: 'jest',
918
+ results: {
919
+ total: result.total,
920
+ passed: result.passed,
921
+ failed: result.failed,
922
+ skipped: result.skipped + result.pending,
923
+ tests: result.tests.map(t => ({
924
+ name: t.name,
925
+ status: t.status,
926
+ duration: t.duration,
927
+ error: t.error
928
+ }))
929
+ },
930
+ junit: this.buildJunitFromJest(result)
931
+ };
932
+ }
933
+
934
+ /**
935
+ * Execute Pytest unit tests gate (v2.2.0)
936
+ */
937
+ private async executePytestGate(gateName: string, gateConfig: any): Promise<GateResult> {
938
+ const adapter = new PytestAdapter(this.workingDir);
939
+
940
+ const config: any = {
941
+ cwd: this.workingDir,
942
+ ...gateConfig.config
943
+ };
944
+
945
+ const result = await adapter.execute(config);
946
+
947
+ return {
948
+ gate: gateName,
949
+ success: result.success,
950
+ duration: result.duration,
951
+ adapter: 'pytest',
952
+ results: {
953
+ total: result.total,
954
+ passed: result.passed,
955
+ failed: result.failed,
956
+ skipped: result.skipped,
957
+ tests: result.tests.map(t => ({
958
+ name: t.name,
959
+ status: t.status,
960
+ duration: t.duration,
961
+ error: t.error
962
+ }))
963
+ },
964
+ junit: this.buildJunitFromPytest(result)
965
+ };
966
+ }
967
+
968
+ /**
969
+ * Build JUnit format from Vitest results
970
+ */
971
+ private buildJunitFromVitest(result: any): any {
972
+ // Convert adapter result to JUnit-like format
973
+ return {
974
+ testsuites: [{
975
+ name: 'vitest',
976
+ tests: result.total,
977
+ failures: result.failed,
978
+ skipped: result.skipped,
979
+ time: result.duration / 1000,
980
+ testcases: result.tests.map((t: any) => ({
981
+ name: t.name,
982
+ classname: t.file,
983
+ time: t.duration / 1000,
984
+ failure: t.error ? { message: t.error } : undefined,
985
+ skipped: t.status === 'skipped' ? { message: 'Skipped' } : undefined
986
+ }))
987
+ }]
988
+ };
989
+ }
990
+
991
+ /**
992
+ * Build JUnit format from Jest results
993
+ */
994
+ private buildJunitFromJest(result: any): any {
995
+ return {
996
+ testsuites: [{
997
+ name: 'jest',
998
+ tests: result.total,
999
+ failures: result.failed,
1000
+ skipped: result.pending,
1001
+ time: result.duration / 1000,
1002
+ testcases: result.tests.map((t: any) => ({
1003
+ name: t.name,
1004
+ time: t.duration / 1000,
1005
+ failure: t.error ? { message: t.error } : undefined,
1006
+ skipped: t.status === 'pending' || t.status === 'skipped' ? { message: 'Skipped' } : undefined
1007
+ }))
1008
+ }]
1009
+ };
1010
+ }
1011
+
1012
+ /**
1013
+ * Build JUnit format from Pytest results
1014
+ */
1015
+ private buildJunitFromPytest(result: any): any {
1016
+ return {
1017
+ testsuites: [{
1018
+ name: 'pytest',
1019
+ tests: result.total,
1020
+ failures: result.failed,
1021
+ skipped: result.skipped,
1022
+ time: result.duration / 1000,
1023
+ testcases: result.tests.map((t: any) => ({
1024
+ name: t.name,
1025
+ classname: t.file,
1026
+ time: t.duration / 1000,
1027
+ failure: t.error ? { message: t.error } : undefined,
1028
+ skipped: t.status === 'skipped' ? { message: 'Skipped' } : undefined
1029
+ }))
1030
+ }]
1031
+ };
1032
+ }
1033
+
734
1034
  /**
735
1035
  * Run API smoke gate
736
1036
  * Uses PlaywrightNativeApiAdapter for zero-overhead HTTP testing
@@ -6,6 +6,26 @@
6
6
  * AI-generated code support and comprehensive authentication.
7
7
  */
8
8
 
9
+ /**
10
+ * Page Object Model configuration
11
+ */
12
+ export interface PageObjectConfigV2 {
13
+ /**
14
+ * Directory containing page object files
15
+ */
16
+ directory?: string;
17
+
18
+ /**
19
+ * Glob pattern for page object files
20
+ */
21
+ pattern?: string;
22
+
23
+ /**
24
+ * Base URL for page objects (can be overridden per page)
25
+ */
26
+ baseUrl?: string;
27
+ }
28
+
9
29
  /**
10
30
  * Pack Configuration v2
11
31
  */
@@ -24,6 +44,10 @@ export interface PackConfigV2 {
24
44
  * - ./fixtures/products.json
25
45
  */
26
46
  fixtures?: string[];
47
+ /**
48
+ * Page Object Model configuration
49
+ */
50
+ pageObjects?: PageObjectConfigV2;
27
51
  auth?: AuthConfigV2;
28
52
  gates: Record<string, GateConfigV2>;
29
53
  hooks?: HooksConfig;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
+ "private": false,
4
5
  "description": "Transform software testing into verifiable, signed, and traceable proofs",
5
6
  "type": "module",
6
7
  "packageManager": "pnpm@9.12.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360-mcp",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "QA360 MCP - Zero-friction CLI + Full MCP Server (23 tools) for integration with industrial DX compliance",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",