@xera-ai/core 0.1.5 → 0.1.7

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.
@@ -335,12 +335,22 @@ async function fetchCmd(argv, opts = {}) {
335
335
  function renderStory(t) {
336
336
  const lines = [];
337
337
  lines.push(`# ${t.key}: ${t.summary}`, "");
338
- lines.push(`## Story`, "", t.story.trim(), "");
338
+ const story = t.story.trim();
339
+ if (/^##\s+story\b/i.test(story)) {
340
+ lines.push(story, "");
341
+ } else {
342
+ lines.push("## Story", "", story, "");
343
+ }
339
344
  if (t.acceptanceCriteria && t.acceptanceCriteria.trim()) {
340
- lines.push(`## Acceptance Criteria`, "", t.acceptanceCriteria.trim(), "");
345
+ const ac = t.acceptanceCriteria.trim();
346
+ if (/^##\s+acceptance\s+criteria\b/i.test(ac)) {
347
+ lines.push(ac, "");
348
+ } else {
349
+ lines.push("## Acceptance Criteria", "", ac, "");
350
+ }
341
351
  }
342
352
  if (t.attachments.length > 0) {
343
- lines.push(`## Attachments`, "", ...t.attachments.map((a) => `- [${a.filename}](${a.url})`), "");
353
+ lines.push("## Attachments", "", ...t.attachments.map((a) => `- [${a.filename}](${a.url})`), "");
344
354
  }
345
355
  return lines.join(`
346
356
  `);
@@ -597,7 +607,7 @@ function needsRefresh(entry, policy, now = new Date) {
597
607
  // src/bin-internal/exec.ts
598
608
  import { stagePlaywrightState, runAuthSetup, runPlaywright } from "@xera-ai/web";
599
609
  import { chromium } from "@playwright/test";
600
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync9 } from "fs";
610
+ import { mkdirSync as mkdirSync7, existsSync as existsSync9 } from "fs";
601
611
  import { join as join5 } from "path";
602
612
  async function execCmd(argv) {
603
613
  const ticket = argv[0];
@@ -649,25 +659,34 @@ async function execCmd(argv) {
649
659
  await browser.close();
650
660
  }
651
661
  }
652
- const stagedRoles = {};
653
662
  if (config.web.auth.strategy === "storageState") {
654
663
  for (const roleName of Object.keys(config.web.auth.roles)) {
655
664
  if (readAuthState(paths.authDir, roleName)) {
656
- stagedRoles[roleName] = stagePlaywrightState(paths.authDir, roleName);
665
+ stagePlaywrightState(paths.authDir, roleName);
657
666
  }
658
667
  }
659
668
  }
660
- const cfgPath = join5(paths.ticketDir, "playwright.config.ts");
669
+ const cfgPath = join5(cwd, "playwright.config.ts");
661
670
  if (!existsSync9(cfgPath)) {
662
- writeFileSync6(cfgPath, renderPlaywrightConfig({
663
- baseUrl: config.web.baseUrl[config.web.defaultEnv],
664
- storageStatePathPerRole: stagedRoles
665
- }));
671
+ console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
672
+ return 1;
666
673
  }
667
674
  const runDir = paths.runPath(runId).runDir;
668
675
  mkdirSync7(runDir, { recursive: true });
669
- log.log({ step: "exec.start", runId });
670
- const r = await runPlaywright({ specPath: paths.specPath, configPath: cfgPath, outputDir: runDir });
676
+ const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
677
+ const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv];
678
+ const reportJsonPath = join5(runDir, "report.json");
679
+ log.log({ step: "exec.start", runId, env: envName, baseURL });
680
+ const r = await runPlaywright({
681
+ specPath: paths.specPath,
682
+ configPath: cfgPath,
683
+ outputDir: runDir,
684
+ env: {
685
+ XERA_BASE_URL: baseURL,
686
+ XERA_ENV: envName,
687
+ PLAYWRIGHT_JSON_OUTPUT_NAME: reportJsonPath
688
+ }
689
+ });
671
690
  log.log({ step: "exec.done", runId, exit: r.exitCode, ms: Date.now() - t0 });
672
691
  console.log(`[xera:exec] runId=${runId} outcome=${r.outcome}`);
673
692
  return r.outcome === "PASS" ? 0 : 3;
@@ -675,20 +694,6 @@ async function execCmd(argv) {
675
694
  releaseLock(paths.lockPath);
676
695
  }
677
696
  }
678
- function renderPlaywrightConfig(opts) {
679
- const projects = Object.entries(opts.storageStatePathPerRole).map(([role, path]) => ` { name: '${role}', use: { ...devices['Desktop Chromium'], storageState: '${path}' } }`);
680
- if (projects.length === 0)
681
- projects.push(` { name: 'default', use: { ...devices['Desktop Chromium'] } }`);
682
- return `import { defineConfig, devices } from '@playwright/test';
683
- export default defineConfig({
684
- use: { baseURL: '${opts.baseUrl}', trace: 'on' },
685
- projects: [
686
- ${projects.join(`,
687
- `)}
688
- ],
689
- });
690
- `;
691
- }
692
697
 
693
698
  // src/bin-internal/normalize.ts
694
699
  import { normalizeRun } from "@xera-ai/web";
@@ -718,7 +723,7 @@ async function normalizeCmd(argv) {
718
723
  }
719
724
 
720
725
  // src/bin-internal/report.ts
721
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
726
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
722
727
  import { join as join7 } from "path";
723
728
 
724
729
  // src/classifier/aggregate.ts
@@ -753,7 +758,7 @@ function aggregateScenarios(scenarios) {
753
758
  import { existsSync as existsSync12 } from "fs";
754
759
 
755
760
  // src/artifact/status.ts
756
- import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8 } from "fs";
761
+ import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "fs";
757
762
  import { dirname as dirname5 } from "path";
758
763
  import { z as z4 } from "zod";
759
764
  var ClassificationEnum = z4.enum(["PASS", "REAL_BUG", "SELECTOR_DRIFT", "FLAKY", "TEST_BUG"]);
@@ -787,7 +792,7 @@ function readStatus(path) {
787
792
  }
788
793
  function writeStatus(path, status) {
789
794
  mkdirSync8(dirname5(path), { recursive: true });
790
- writeFileSync7(path, JSON.stringify(status, null, 2));
795
+ writeFileSync6(path, JSON.stringify(status, null, 2));
791
796
  }
792
797
  function appendHistory(path, entry) {
793
798
  const s = readStatus(path);
@@ -884,7 +889,7 @@ async function reportCmd(argv) {
884
889
  promptsVersion: "1.0.0"
885
890
  });
886
891
  const draftPath = join7(paths.ticketDir, "jira-comment.draft.md");
887
- writeFileSync8(draftPath, md);
892
+ writeFileSync7(draftPath, md);
888
893
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
889
894
  return 0;
890
895
  }
@@ -1 +1 @@
1
- {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/bin-internal/exec.ts"],"names":[],"mappings":"AAWA,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAoF7D"}
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/bin-internal/exec.ts"],"names":[],"mappings":"AAWA,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuG7D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/core",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -6,7 +6,7 @@ import { readAuthState } from '../auth/state';
6
6
  import { needsRefresh } from '../auth/refresh';
7
7
  import { stagePlaywrightState, runAuthSetup, runPlaywright } from '@xera-ai/web';
8
8
  import { chromium } from '@playwright/test';
9
- import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
9
+ import { mkdirSync, existsSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
 
12
12
  export async function execCmd(argv: string[]): Promise<number> {
@@ -61,30 +61,49 @@ export async function execCmd(argv: string[]): Promise<number> {
61
61
  }
62
62
  }
63
63
 
64
- // Stage Playwright storageState files for declared roles
65
- const stagedRoles: Record<string, string> = {};
64
+ // Stage Playwright storageState files at predictable paths
65
+ // (.xera/.auth/.cache/<role>.json) generated spec.ts references these
66
+ // via test.use({ storageState }) when an authenticated session is needed.
66
67
  if (config.web.auth.strategy === 'storageState') {
67
68
  for (const roleName of Object.keys(config.web.auth.roles)) {
68
69
  if (readAuthState(paths.authDir, roleName)) {
69
- stagedRoles[roleName] = stagePlaywrightState(paths.authDir, roleName);
70
+ stagePlaywrightState(paths.authDir, roleName);
70
71
  }
71
72
  }
72
73
  }
73
74
 
74
- // Generate per-run playwright.config.ts if not present at ticketDir
75
- const cfgPath = join(paths.ticketDir, 'playwright.config.ts');
75
+ // Use the root playwright.config.ts (generated by `xera init`). We never
76
+ // emit a per-ticket config — that path duplicates the root and bit-rots.
77
+ const cfgPath = join(cwd, 'playwright.config.ts');
76
78
  if (!existsSync(cfgPath)) {
77
- writeFileSync(cfgPath, renderPlaywrightConfig({
78
- baseUrl: config.web.baseUrl[config.web.defaultEnv]!,
79
- storageStatePathPerRole: stagedRoles,
80
- }));
79
+ console.error(
80
+ `[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`,
81
+ );
82
+ return 1;
81
83
  }
82
84
 
83
85
  const runDir = paths.runPath(runId).runDir;
84
86
  mkdirSync(runDir, { recursive: true });
85
87
 
86
- log.log({ step: 'exec.start', runId });
87
- const r = await runPlaywright({ specPath: paths.specPath, configPath: cfgPath, outputDir: runDir });
88
+ const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
89
+ const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv]!;
90
+
91
+ const reportJsonPath = join(runDir, 'report.json');
92
+
93
+ log.log({ step: 'exec.start', runId, env: envName, baseURL });
94
+ const r = await runPlaywright({
95
+ specPath: paths.specPath,
96
+ configPath: cfgPath,
97
+ outputDir: runDir,
98
+ env: {
99
+ XERA_BASE_URL: baseURL,
100
+ XERA_ENV: envName,
101
+ // Playwright's JSON reporter prints to stdout by default. Redirect it
102
+ // to a file inside the run dir so xera:normalize has a deterministic
103
+ // path to read.
104
+ PLAYWRIGHT_JSON_OUTPUT_NAME: reportJsonPath,
105
+ },
106
+ });
88
107
  log.log({ step: 'exec.done', runId, exit: r.exitCode, ms: Date.now() - t0 });
89
108
 
90
109
  console.log(`[xera:exec] runId=${runId} outcome=${r.outcome}`);
@@ -95,17 +114,3 @@ export async function execCmd(argv: string[]): Promise<number> {
95
114
  }
96
115
  }
97
116
 
98
- function renderPlaywrightConfig(opts: { baseUrl: string; storageStatePathPerRole: Record<string, string> }): string {
99
- const projects = Object.entries(opts.storageStatePathPerRole).map(
100
- ([role, path]) => ` { name: '${role}', use: { ...devices['Desktop Chromium'], storageState: '${path}' } }`,
101
- );
102
- if (projects.length === 0) projects.push(` { name: 'default', use: { ...devices['Desktop Chromium'] } }`);
103
- return `import { defineConfig, devices } from '@playwright/test';
104
- export default defineConfig({
105
- use: { baseURL: '${opts.baseUrl}', trace: 'on' },
106
- projects: [
107
- ${projects.join(',\n')}
108
- ],
109
- });
110
- `;
111
- }
@@ -60,12 +60,25 @@ export async function fetchCmd(argv: string[], opts: FetchCmdOpts = {}): Promise
60
60
  function renderStory(t: JiraTicket): string {
61
61
  const lines: string[] = [];
62
62
  lines.push(`# ${t.key}: ${t.summary}`, '');
63
- lines.push(`## Story`, '', t.story.trim(), '');
63
+
64
+ const story = t.story.trim();
65
+ // Avoid double "## Story" heading when Jira description already starts with it.
66
+ if (/^##\s+story\b/i.test(story)) {
67
+ lines.push(story, '');
68
+ } else {
69
+ lines.push('## Story', '', story, '');
70
+ }
71
+
64
72
  if (t.acceptanceCriteria && t.acceptanceCriteria.trim()) {
65
- lines.push(`## Acceptance Criteria`, '', t.acceptanceCriteria.trim(), '');
73
+ const ac = t.acceptanceCriteria.trim();
74
+ if (/^##\s+acceptance\s+criteria\b/i.test(ac)) {
75
+ lines.push(ac, '');
76
+ } else {
77
+ lines.push('## Acceptance Criteria', '', ac, '');
78
+ }
66
79
  }
67
80
  if (t.attachments.length > 0) {
68
- lines.push(`## Attachments`, '', ...t.attachments.map(a => `- [${a.filename}](${a.url})`), '');
81
+ lines.push('## Attachments', '', ...t.attachments.map((a) => `- [${a.filename}](${a.url})`), '');
69
82
  }
70
83
  return lines.join('\n');
71
84
  }