@asrunjs/asrun 0.0.0-stage → 0.5.2
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/.claude/commands/explore-case.md +18 -0
- package/.claude/commands/gen-spec.md +16 -0
- package/.claude/commands/heal-spec.md +26 -0
- package/.claude/commands/make-case.md +11 -0
- package/.claude/settings.json +5 -0
- package/.claude/skills/exploring-cases/SKILL.md +146 -0
- package/.claude/skills/exploring-cases/references/assertion-targets.md +103 -0
- package/.claude/skills/exploring-cases/references/handoff-card.js +128 -0
- package/.claude/skills/exploring-cases/references/open-questions.md +116 -0
- package/.claude/skills/exploring-cases/references/readiness.md +146 -0
- package/.claude/skills/exploring-cases/references/user-handoff.md +157 -0
- package/.claude/skills/generating-specs/SKILL.md +219 -0
- package/.claude/skills/generating-specs/references/conventions.md +148 -0
- package/.claude/skills/making-cases/SKILL.md +72 -0
- package/.claude/skills/making-cases/references/gaps.md +36 -0
- package/.claude/skills/making-cases/references/normalizing.md +39 -0
- package/.claude/skills/making-cases/references/splitting.md +28 -0
- package/.claude/skills/making-cases/references/writes-and-email.md +37 -0
- package/AGENTS.md +159 -0
- package/CLAUDE.md +2 -0
- package/LICENSE +21 -0
- package/README.md +511 -2
- package/bin/asrun.mjs +49 -0
- package/dist/lib/account-lock.d.ts +6 -0
- package/dist/lib/account-lock.js +52 -0
- package/dist/lib/assertions.d.ts +14 -0
- package/dist/lib/assertions.js +39 -0
- package/dist/lib/baselines.d.ts +43 -0
- package/dist/lib/baselines.js +108 -0
- package/dist/lib/build-artifacts.d.ts +8 -0
- package/dist/lib/build-artifacts.js +10 -0
- package/dist/lib/case-doc.d.ts +311 -0
- package/dist/lib/case-doc.js +1117 -0
- package/dist/lib/case-id.d.ts +11 -0
- package/dist/lib/case-id.js +17 -0
- package/dist/lib/case-ops.d.ts +26 -0
- package/dist/lib/case-ops.js +110 -0
- package/dist/lib/cases.d.ts +16 -0
- package/dist/lib/cases.js +51 -0
- package/dist/lib/cli.d.ts +10 -0
- package/dist/lib/cli.js +18 -0
- package/dist/lib/command.d.ts +24 -0
- package/dist/lib/command.js +90 -0
- package/dist/lib/downloads.d.ts +42 -0
- package/dist/lib/downloads.js +84 -0
- package/dist/lib/email.d.ts +10 -0
- package/dist/lib/email.js +19 -0
- package/dist/lib/ffmpeg.d.ts +8 -0
- package/dist/lib/ffmpeg.js +50 -0
- package/dist/lib/fixtures.d.ts +40 -0
- package/dist/lib/fixtures.js +149 -0
- package/dist/lib/login-card.d.ts +22 -0
- package/dist/lib/login-card.js +183 -0
- package/dist/lib/node-cli.d.ts +15 -0
- package/dist/lib/node-cli.js +36 -0
- package/dist/lib/paths.d.ts +25 -0
- package/dist/lib/paths.js +88 -0
- package/dist/lib/pdf.d.ts +71 -0
- package/dist/lib/pdf.js +140 -0
- package/dist/lib/profiles.d.ts +62 -0
- package/dist/lib/profiles.js +174 -0
- package/dist/lib/ready.d.ts +36 -0
- package/dist/lib/ready.js +53 -0
- package/dist/lib/records.d.ts +33 -0
- package/dist/lib/records.js +61 -0
- package/dist/lib/run-filters.d.ts +19 -0
- package/dist/lib/run-filters.js +38 -0
- package/dist/lib/run-summary.d.ts +90 -0
- package/dist/lib/run-summary.js +35 -0
- package/dist/lib/session.d.ts +30 -0
- package/dist/lib/session.js +111 -0
- package/dist/lib/summary-reporter.d.ts +16 -0
- package/dist/lib/summary-reporter.js +146 -0
- package/dist/lib/targets.d.ts +19 -0
- package/dist/lib/targets.js +26 -0
- package/dist/lib/ui-build.d.ts +10 -0
- package/dist/lib/ui-build.js +30 -0
- package/dist/lib/values.d.ts +29 -0
- package/dist/lib/values.js +85 -0
- package/dist/lib/write-gate.d.ts +41 -0
- package/dist/lib/write-gate.js +129 -0
- package/dist/playwright.config.d.ts +2 -0
- package/dist/playwright.config.js +92 -0
- package/dist/scripts/build.d.ts +1 -0
- package/dist/scripts/build.js +21 -0
- package/dist/scripts/check-cases.d.ts +1 -0
- package/dist/scripts/check-cases.js +149 -0
- package/dist/scripts/cleanup.d.ts +1 -0
- package/dist/scripts/cleanup.js +25 -0
- package/dist/scripts/cli.d.ts +1 -0
- package/dist/scripts/cli.js +28 -0
- package/dist/scripts/doctor.d.ts +1 -0
- package/dist/scripts/doctor.js +296 -0
- package/dist/scripts/explore.d.ts +1 -0
- package/dist/scripts/explore.js +75 -0
- package/dist/scripts/go.mjs +188 -0
- package/dist/scripts/heal-evidence.mjs +143 -0
- package/dist/scripts/init.d.ts +22 -0
- package/dist/scripts/init.js +332 -0
- package/dist/scripts/lint-specs.d.ts +1 -0
- package/dist/scripts/lint-specs.js +141 -0
- package/dist/scripts/login.d.ts +1 -0
- package/dist/scripts/login.js +75 -0
- package/dist/scripts/new-case.d.ts +1 -0
- package/dist/scripts/new-case.js +19 -0
- package/dist/scripts/render-pdf.d.ts +1 -0
- package/dist/scripts/render-pdf.js +44 -0
- package/dist/scripts/report.d.ts +1 -0
- package/dist/scripts/report.js +14 -0
- package/dist/scripts/review-baselines.d.ts +1 -0
- package/dist/scripts/review-baselines.js +30 -0
- package/dist/scripts/run.d.ts +1 -0
- package/dist/scripts/run.js +423 -0
- package/dist/scripts/status.d.ts +1 -0
- package/dist/scripts/status.js +31 -0
- package/dist/scripts/ui-build.d.ts +1 -0
- package/dist/scripts/ui-build.js +7 -0
- package/dist/scripts/ui-dev.d.ts +1 -0
- package/dist/scripts/ui-dev.js +3 -0
- package/dist/scripts/ui.d.ts +1 -0
- package/dist/scripts/ui.js +13 -0
- package/dist/ui/dev.d.ts +1 -0
- package/dist/ui/dev.js +36 -0
- package/dist/ui/server/actions.d.ts +95 -0
- package/dist/ui/server/actions.js +365 -0
- package/dist/ui/server/ai-models.d.ts +3 -0
- package/dist/ui/server/ai-models.js +347 -0
- package/dist/ui/server/ai-stream.d.ts +66 -0
- package/dist/ui/server/ai-stream.js +408 -0
- package/dist/ui/server/app.d.ts +4 -0
- package/dist/ui/server/app.js +395 -0
- package/dist/ui/server/cases.d.ts +23 -0
- package/dist/ui/server/cases.js +170 -0
- package/dist/ui/server/files.d.ts +5 -0
- package/dist/ui/server/files.js +67 -0
- package/dist/ui/server/index.d.ts +1 -0
- package/dist/ui/server/index.js +33 -0
- package/dist/ui/server/jobs.d.ts +80 -0
- package/dist/ui/server/jobs.js +317 -0
- package/dist/ui/server/profiles.d.ts +35 -0
- package/dist/ui/server/profiles.js +371 -0
- package/dist/ui/server/watch-files.d.ts +4 -0
- package/dist/ui/server/watch-files.js +36 -0
- package/dist/ui/shared/ai-commands.d.ts +88 -0
- package/dist/ui/shared/ai-commands.js +177 -0
- package/dist/ui/shared/case-id.d.ts +1 -0
- package/dist/ui/shared/case-id.js +1 -0
- package/dist/ui/shared/jobs.d.ts +6 -0
- package/dist/ui/shared/jobs.js +7 -0
- package/dist/ui/shared/product.d.ts +3 -0
- package/dist/ui/shared/product.js +3 -0
- package/dist/ui/shared/run-filters.d.ts +1 -0
- package/dist/ui/shared/run-filters.js +1 -0
- package/dist/ui/shared/site-address.d.ts +6 -0
- package/dist/ui/shared/site-address.js +11 -0
- package/dist/ui/shared/types.d.ts +371 -0
- package/dist/ui/shared/types.js +1 -0
- package/docs/agents/framework-dev.md +79 -0
- package/docs/theme-guide.html +922 -0
- package/package.json +109 -3
- package/templates/.claude/commands/explore-case.md +22 -0
- package/templates/.claude/commands/gen-spec.md +21 -0
- package/templates/.claude/commands/heal-spec.md +31 -0
- package/templates/.claude/commands/make-case.md +13 -0
- package/templates/.claude/settings.json +5 -0
- package/templates/.mcp.json +8 -0
- package/templates/AGENTS.md +64 -0
- package/templates/CLAUDE.md +1 -0
- package/templates/cases/_template.md +57 -0
- package/templates/gitignore +13 -0
- package/templates/opencode.jsonc +16 -0
- package/templates/package.json +31 -0
- package/templates/playwright.config.ts +3 -0
- package/templates/pnpm-workspace.yaml +5 -0
- package/templates/profiles.yaml +42 -0
- package/templates/seed.spec.ts +9 -0
- package/templates/tsconfig.json +14 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/assets/apl-gfMMiTNf.js +1 -0
- package/ui/dist/assets/asciiarmor-C_eisQCp.js +1 -0
- package/ui/dist/assets/asn1-DCl_8MCW.js +1 -0
- package/ui/dist/assets/asterisk-BEn0Cwpx.js +1 -0
- package/ui/dist/assets/brainfuck-D5EjA2JK.js +1 -0
- package/ui/dist/assets/clike-xvONpD0Z.js +1 -0
- package/ui/dist/assets/clojure-BflJGmDX.js +1 -0
- package/ui/dist/assets/cmake-84nbDH0I.js +1 -0
- package/ui/dist/assets/cobol-DHLQUbN7.js +1 -0
- package/ui/dist/assets/coffeescript-De_zI4J0.js +1 -0
- package/ui/dist/assets/commonlisp-DgAKBe0r.js +1 -0
- package/ui/dist/assets/crystal-Bl6YKCG_.js +1 -0
- package/ui/dist/assets/css-CnaYoQib.js +1 -0
- package/ui/dist/assets/cypher-p9eYesGn.js +1 -0
- package/ui/dist/assets/d-DWkQyNnU.js +1 -0
- package/ui/dist/assets/diff-ChtP43wD.js +1 -0
- package/ui/dist/assets/dist-BGbe0-Aa.js +1 -0
- package/ui/dist/assets/dist-Ba-HC662.js +2 -0
- package/ui/dist/assets/dist-BtMLMthJ.js +1 -0
- package/ui/dist/assets/dist-Bw5-uxs9.js +1 -0
- package/ui/dist/assets/dist-BzulKuec.js +1 -0
- package/ui/dist/assets/dist-C-Oyo1ZT.js +1 -0
- package/ui/dist/assets/dist-C8MdhqgG.js +1 -0
- package/ui/dist/assets/dist-CG1pHn5m.js +1 -0
- package/ui/dist/assets/dist-CgIQYY1o.js +1 -0
- package/ui/dist/assets/dist-CmpteMYY.js +9 -0
- package/ui/dist/assets/dist-DDJNvivM.js +1 -0
- package/ui/dist/assets/dist-DF3F_I8N.js +1 -0
- package/ui/dist/assets/dist-DSy3dVf4.js +11 -0
- package/ui/dist/assets/dist-Dlorc3Sx.js +1 -0
- package/ui/dist/assets/dist-Dq6sW5Ur.js +1 -0
- package/ui/dist/assets/dist-Dr9crbBz.js +13 -0
- package/ui/dist/assets/dist-jjxQutEm.js +1 -0
- package/ui/dist/assets/dockerfile-DJkHQgkl.js +1 -0
- package/ui/dist/assets/dtd-DTF72YNO.js +1 -0
- package/ui/dist/assets/dylan-S7ucR4pR.js +1 -0
- package/ui/dist/assets/ebnf-D4c0_ac3.js +1 -0
- package/ui/dist/assets/ecl-BVIeWmwY.js +1 -0
- package/ui/dist/assets/eiffel-BEjRio4Q.js +1 -0
- package/ui/dist/assets/elm-Cshzl8qu.js +1 -0
- package/ui/dist/assets/erlang-BV0c4Hdn.js +1 -0
- package/ui/dist/assets/factor-LKH5gN8L.js +1 -0
- package/ui/dist/assets/fcl-ClOhbFR7.js +1 -0
- package/ui/dist/assets/forth-D6RzAl64.js +1 -0
- package/ui/dist/assets/fortran-Bt6PBEDR.js +1 -0
- package/ui/dist/assets/gas-BHEdbvp9.js +1 -0
- package/ui/dist/assets/gherkin-oBAE_ms0.js +1 -0
- package/ui/dist/assets/groovy-CUrWKPia.js +1 -0
- package/ui/dist/assets/haskell-BRsoo5mP.js +1 -0
- package/ui/dist/assets/haxe-DjVOVvNP.js +1 -0
- package/ui/dist/assets/http-CLVgA2GD.js +1 -0
- package/ui/dist/assets/idl-BUZw3wgd.js +1 -0
- package/ui/dist/assets/index-CTmnF0Wp.css +2 -0
- package/ui/dist/assets/index-P_Yo0ZZn.js +338 -0
- package/ui/dist/assets/javascript-lS2r2ifg.js +1 -0
- package/ui/dist/assets/julia-CQfgDRfP.js +1 -0
- package/ui/dist/assets/livescript-BXxG0Yva.js +1 -0
- package/ui/dist/assets/lua-DPCEkfD_.js +1 -0
- package/ui/dist/assets/mathematica-u8YMmbU0.js +1 -0
- package/ui/dist/assets/mbox-BEMVcqjs.js +1 -0
- package/ui/dist/assets/mirc-C9zpzAZU.js +1 -0
- package/ui/dist/assets/mllike-B45xgp2S.js +1 -0
- package/ui/dist/assets/modelica-CqHI_q-D.js +1 -0
- package/ui/dist/assets/mscgen-qgSLujhx.js +1 -0
- package/ui/dist/assets/mumps-BSzj6Xyz.js +1 -0
- package/ui/dist/assets/nginx-C-ORUuei.js +1 -0
- package/ui/dist/assets/nsis-viPvXFiq.js +1 -0
- package/ui/dist/assets/ntriples-CzMBulkp.js +1 -0
- package/ui/dist/assets/octave-D2Q8cUp1.js +1 -0
- package/ui/dist/assets/oz-BwTct_5T.js +1 -0
- package/ui/dist/assets/pascal-Dp_KdFgX.js +1 -0
- package/ui/dist/assets/perl-BkSkiI9m.js +1 -0
- package/ui/dist/assets/pig-AF4Hwhju.js +1 -0
- package/ui/dist/assets/powershell-D8WpQPaX.js +1 -0
- package/ui/dist/assets/properties-C357ku5U.js +1 -0
- package/ui/dist/assets/protobuf-D1ij_kNL.js +1 -0
- package/ui/dist/assets/pug-BqCHZ5xT.js +1 -0
- package/ui/dist/assets/puppet-BZyAS7I4.js +1 -0
- package/ui/dist/assets/python-B_1Jd8HM.js +1 -0
- package/ui/dist/assets/q-B9NhS7L_.js +1 -0
- package/ui/dist/assets/r-CxDmU1I4.js +1 -0
- package/ui/dist/assets/rpm-gpDK5sbt.js +1 -0
- package/ui/dist/assets/ruby-ChfNwlCY.js +1 -0
- package/ui/dist/assets/sas-C2OyD2Y6.js +1 -0
- package/ui/dist/assets/scheme-D9qYN0V5.js +1 -0
- package/ui/dist/assets/shell-DwuoZtxw.js +1 -0
- package/ui/dist/assets/sieve-tLWDfEkZ.js +1 -0
- package/ui/dist/assets/simple-mode-DXiLM1-G.js +1 -0
- package/ui/dist/assets/smalltalk-BLp5-jwC.js +1 -0
- package/ui/dist/assets/solr-B78wSQJQ.js +1 -0
- package/ui/dist/assets/sparql-Dz-mlCAC.js +1 -0
- package/ui/dist/assets/spreadsheet-CTIncYxj.js +1 -0
- package/ui/dist/assets/sql-DuQMFDvy.js +1 -0
- package/ui/dist/assets/stex-DNzd62Q_.js +1 -0
- package/ui/dist/assets/stylus-BOVgKnng.js +1 -0
- package/ui/dist/assets/swift-DKB6_1j6.js +1 -0
- package/ui/dist/assets/tcl-BGfruO1u.js +1 -0
- package/ui/dist/assets/textile-C4jVoHG1.js +1 -0
- package/ui/dist/assets/tiddlywiki-VlUiK86J.js +1 -0
- package/ui/dist/assets/tiki-CvUEFusT.js +1 -0
- package/ui/dist/assets/toml-BPTmHmyx.js +1 -0
- package/ui/dist/assets/troff-C2dAgh7P.js +1 -0
- package/ui/dist/assets/ttcn-PPSTk1ow.js +1 -0
- package/ui/dist/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
- package/ui/dist/assets/turtle-Cx3rYX2g.js +1 -0
- package/ui/dist/assets/vb-BthdM_4N.js +1 -0
- package/ui/dist/assets/vbscript-D9f6rSsU.js +1 -0
- package/ui/dist/assets/velocity-CqftRsjg.js +1 -0
- package/ui/dist/assets/verilog-CjI9Y6RI.js +1 -0
- package/ui/dist/assets/vhdl-kS471soi.js +1 -0
- package/ui/dist/assets/webidl-D5jiJvOU.js +1 -0
- package/ui/dist/assets/xquery-BKJx4mf9.js +1 -0
- package/ui/dist/assets/yacas-CXXgtw0p.js +1 -0
- package/ui/dist/assets/z80-Co3PbHdE.js +1 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/favicon.svg +11 -0
- package/ui/dist/help/ai-tools.png +0 -0
- package/ui/dist/help/case-explore.png +0 -0
- package/ui/dist/help/case-generate.png +0 -0
- package/ui/dist/help/cases-memo.png +0 -0
- package/ui/dist/help/profile-form.png +0 -0
- package/ui/dist/help/profiles-login.png +0 -0
- package/ui/dist/help/reports.png +0 -0
- package/ui/dist/help/run-tests.png +0 -0
- package/ui/dist/index.html +23 -0
- package/ui/dist/logos/claude.svg +1 -0
- package/ui/dist/logos/opencode.svg +1 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type RunFilters } from './run-filters.js';
|
|
2
|
+
/** Set by `pnpm asrun` once a human confirmed that cases may write test data in this (protected) environment. */
|
|
3
|
+
export declare const ALLOW_WRITES_ENV = "ASRUN_ALLOW_WRITES";
|
|
4
|
+
/** Every environment except staging needs an explicit permission before a case may write data. */
|
|
5
|
+
export declare const isProtectedEnv: (env: string) => boolean;
|
|
6
|
+
export interface WriteCase {
|
|
7
|
+
id: string;
|
|
8
|
+
note: string;
|
|
9
|
+
/** Profiles whose test data the case will write to in this run. */
|
|
10
|
+
profiles: string[];
|
|
11
|
+
}
|
|
12
|
+
/** The write cases of a run, split by whether the environment is one the case may write in. */
|
|
13
|
+
export interface WriteCases {
|
|
14
|
+
/** The case lists this environment under `writes`: it writes here, after the confirmation in a protected one. */
|
|
15
|
+
writing: WriteCase[];
|
|
16
|
+
/** The case writes only in other environments: it is skipped here with a notice, not failed. */
|
|
17
|
+
skipped: WriteCase[];
|
|
18
|
+
}
|
|
19
|
+
/** Can a case run in this environment at all (`envs` in its front matter)? */
|
|
20
|
+
export declare function caseRunsIn(caseId: string, env: string): boolean;
|
|
21
|
+
/** One case of a run and the profiles it runs for. `skip` says why it is skipped in this environment, if it is. */
|
|
22
|
+
export interface PlannedCase {
|
|
23
|
+
id: string;
|
|
24
|
+
profiles: string[];
|
|
25
|
+
writes: boolean;
|
|
26
|
+
skip: string | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Every case that a run with these filters would list, as Playwright itself reports it (`--list`), so project, grep
|
|
30
|
+
* and suite filters are applied exactly as in the real run. Async, so a slow list of tests never blocks the UI server.
|
|
31
|
+
* Rejects with a readable message when Playwright cannot list the tests (for example an invalid grep expression).
|
|
32
|
+
*/
|
|
33
|
+
export declare function planRun(filters: RunFilters): Promise<PlannedCase[]>;
|
|
34
|
+
/**
|
|
35
|
+
* The cases that a run with these filters would execute and that write data, split into those that write in this
|
|
36
|
+
* environment and those that are skipped here. Asks Playwright itself
|
|
37
|
+
* (`--list`), so project, grep and suite filters are applied exactly as in the real run.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findWriteCases(filters: RunFilters): WriteCases;
|
|
40
|
+
export declare function describeWriteCases(env: string, cases: WriteCase[]): string;
|
|
41
|
+
export declare function describeSkippedWriteCases(env: string, cases: WriteCase[]): string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { STAGING } from './profiles.js';
|
|
3
|
+
import { caseWrites, caseWritesIn, readCaseMeta } from './cases.js';
|
|
4
|
+
import { commandScript } from './node-cli.js';
|
|
5
|
+
import { ROOT } from './paths.js';
|
|
6
|
+
import { filterArgs } from './run-filters.js';
|
|
7
|
+
import { caseIdFromFile } from './run-summary.js';
|
|
8
|
+
/** Set by `pnpm asrun` once a human confirmed that cases may write test data in this (protected) environment. */
|
|
9
|
+
export const ALLOW_WRITES_ENV = 'ASRUN_ALLOW_WRITES';
|
|
10
|
+
/** Every environment except staging needs an explicit permission before a case may write data. */
|
|
11
|
+
export const isProtectedEnv = (env) => env !== STAGING;
|
|
12
|
+
/** Can a case run in this environment at all (`envs` in its front matter)? */
|
|
13
|
+
export function caseRunsIn(caseId, env) {
|
|
14
|
+
const { envs } = readCaseMeta(caseId);
|
|
15
|
+
return envs === null || envs.includes(env);
|
|
16
|
+
}
|
|
17
|
+
const PLAYWRIGHT = commandScript('@playwright/test', 'playwright');
|
|
18
|
+
const listArgs = (filters) => ['test', '--list', '--reporter=json', ...filterArgs(filters)];
|
|
19
|
+
const listEnv = (env) => ({ ...process.env, ASRUN_ENV: env });
|
|
20
|
+
/** The profiles that a `--list` report schedules for each case. Throws when Playwright reported a real error. */
|
|
21
|
+
function profilesByCaseOf(stdout, stderr) {
|
|
22
|
+
let report;
|
|
23
|
+
try {
|
|
24
|
+
report = JSON.parse(stdout);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`Could not list the tests: ${(stderr || stdout).trim().slice(0, 400)}`);
|
|
28
|
+
}
|
|
29
|
+
// "No tests found" is an empty result, not a failure; anything else (an invalid expression, a broken spec) is.
|
|
30
|
+
const problem = (report.errors ?? []).find((error) => !/No tests found/.test(error.message ?? ''));
|
|
31
|
+
if (problem)
|
|
32
|
+
throw new Error(String(problem.message ?? 'Playwright could not list the tests.').trim().slice(0, 400));
|
|
33
|
+
const profilesByCase = new Map();
|
|
34
|
+
const visit = (suite) => {
|
|
35
|
+
for (const spec of suite.specs ?? []) {
|
|
36
|
+
const caseId = caseIdFromFile(spec.file);
|
|
37
|
+
for (const test of spec.tests ?? []) {
|
|
38
|
+
const profile = String(test.projectName);
|
|
39
|
+
if (!profilesByCase.has(caseId))
|
|
40
|
+
profilesByCase.set(caseId, new Set());
|
|
41
|
+
profilesByCase.get(caseId).add(profile);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const child of suite.suites ?? [])
|
|
45
|
+
visit(child);
|
|
46
|
+
};
|
|
47
|
+
for (const suite of report.suites ?? [])
|
|
48
|
+
visit(suite);
|
|
49
|
+
return profilesByCase;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Every case that a run with these filters would list, as Playwright itself reports it (`--list`), so project, grep
|
|
53
|
+
* and suite filters are applied exactly as in the real run. Async, so a slow list of tests never blocks the UI server.
|
|
54
|
+
* Rejects with a readable message when Playwright cannot list the tests (for example an invalid grep expression).
|
|
55
|
+
*/
|
|
56
|
+
export async function planRun(filters) {
|
|
57
|
+
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn(process.execPath, [PLAYWRIGHT, ...listArgs(filters)], { cwd: ROOT, env: listEnv(filters.env) });
|
|
59
|
+
let out = '';
|
|
60
|
+
let err = '';
|
|
61
|
+
child.stdout.on('data', (chunk) => (out += chunk));
|
|
62
|
+
child.stderr.on('data', (chunk) => (err += chunk));
|
|
63
|
+
child.on('error', reject);
|
|
64
|
+
// The exit code is not checked: "no tests found" exits with 1 but still prints a valid (empty) report.
|
|
65
|
+
child.on('close', () => resolve({ stdout: out, stderr: err }));
|
|
66
|
+
});
|
|
67
|
+
const planned = [];
|
|
68
|
+
for (const [id, profiles] of profilesByCaseOf(stdout, stderr)) {
|
|
69
|
+
let meta;
|
|
70
|
+
try {
|
|
71
|
+
meta = readCaseMeta(id);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue; // seed.spec.ts and other specs without a case document
|
|
75
|
+
}
|
|
76
|
+
const skip = !caseRunsIn(id, filters.env)
|
|
77
|
+
? `Limited to: ${meta.envs?.join(', ')}`
|
|
78
|
+
: caseWrites(meta) && !caseWritesIn(meta, filters.env)
|
|
79
|
+
? `Writes data, no permission in ${filters.env.toUpperCase()}`
|
|
80
|
+
: null;
|
|
81
|
+
planned.push({ id, profiles: [...profiles].sort(), writes: caseWrites(meta), skip });
|
|
82
|
+
}
|
|
83
|
+
return planned.sort((a, b) => a.id.localeCompare(b.id));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The cases that a run with these filters would execute and that write data, split into those that write in this
|
|
87
|
+
* environment and those that are skipped here. Asks Playwright itself
|
|
88
|
+
* (`--list`), so project, grep and suite filters are applied exactly as in the real run.
|
|
89
|
+
*/
|
|
90
|
+
export function findWriteCases(filters) {
|
|
91
|
+
const env = filters.env;
|
|
92
|
+
const result = spawnSync(process.execPath, [PLAYWRIGHT, ...listArgs(filters)], {
|
|
93
|
+
cwd: ROOT,
|
|
94
|
+
encoding: 'utf8',
|
|
95
|
+
env: listEnv(env),
|
|
96
|
+
maxBuffer: 50_000_000,
|
|
97
|
+
});
|
|
98
|
+
const profilesByCase = profilesByCaseOf(result.stdout, result.stderr);
|
|
99
|
+
const writing = [];
|
|
100
|
+
const skipped = [];
|
|
101
|
+
for (const [id, profiles] of profilesByCase) {
|
|
102
|
+
let meta;
|
|
103
|
+
try {
|
|
104
|
+
meta = readCaseMeta(id);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
continue; // seed.spec.ts and other specs without a case document
|
|
108
|
+
}
|
|
109
|
+
if (!caseWrites(meta) || !caseRunsIn(id, env))
|
|
110
|
+
continue;
|
|
111
|
+
(caseWritesIn(meta, env) ? writing : skipped).push({ id, note: meta.writesNote ?? '', profiles: [...profiles].sort() });
|
|
112
|
+
}
|
|
113
|
+
const byId = (a, b) => a.id.localeCompare(b.id);
|
|
114
|
+
return { writing: writing.sort(byId), skipped: skipped.sort(byId) };
|
|
115
|
+
}
|
|
116
|
+
export function describeWriteCases(env, cases) {
|
|
117
|
+
return [
|
|
118
|
+
`${cases.length} case(s) in this run write data in ${env.toUpperCase()}:`,
|
|
119
|
+
...cases.flatMap((item) => [` - ${item.id} (profiles: ${item.profiles.join(', ')})`, ` ${item.note}`]),
|
|
120
|
+
'They only touch data of the test accounts. They never send email in production.',
|
|
121
|
+
].join('\n');
|
|
122
|
+
}
|
|
123
|
+
export function describeSkippedWriteCases(env, cases) {
|
|
124
|
+
return [
|
|
125
|
+
`Skipped: ${cases.length} case(s) write data but have no permission to write in ${env.toUpperCase()}:`,
|
|
126
|
+
...cases.map((item) => ` - ${item.id}`),
|
|
127
|
+
`To allow one, add "${env}" to "writes" in its front matter (and make sure it cleans up).`,
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
4
|
+
import { listProfiles } from './lib/profiles.js';
|
|
5
|
+
import { authFile, isFrameworkRoot, REPORTS_DIR, ROOT, TEMPLATES_DIR, TMP_DIR } from './lib/paths.js';
|
|
6
|
+
import { parseNotLoggedIn } from './lib/session.js';
|
|
7
|
+
import { PRODUCT_NAME } from './ui/shared/product.js';
|
|
8
|
+
const runId = process.env.ASRUN_RUN_ID ?? 'adhoc';
|
|
9
|
+
// Set by `pnpm asrun --env <env>`. Without it (MCP exploration, `--list`) every profile is a project.
|
|
10
|
+
const runEnv = process.env.ASRUN_ENV;
|
|
11
|
+
const stepText = process.env.ASRUN_STEP_TEXT === '1';
|
|
12
|
+
// Exploration through the MCP (and `--list`) has no environment. Nobody needs videos, traces or screenshots there,
|
|
13
|
+
// and encoding a video of a long, busy session can stall the browser.
|
|
14
|
+
const recording = runEnv !== undefined;
|
|
15
|
+
const notLoggedIn = parseNotLoggedIn();
|
|
16
|
+
const profiles = listProfiles();
|
|
17
|
+
const selected = profiles.filter((profile) => !runEnv || profile.env === runEnv);
|
|
18
|
+
if (profiles.length === 0) {
|
|
19
|
+
throw new Error(`No profiles in ${path.join(ROOT, 'profiles.yaml')}. Add one entry per environment (see ` +
|
|
20
|
+
`${path.join(TEMPLATES_DIR, 'profiles.yaml')}), then log in with "pnpm signin <profile-id>".` +
|
|
21
|
+
(isFrameworkRoot()
|
|
22
|
+
? ' This looks like the framework folder itself: run the command in a project that uses the framework, or set ASRUN_ROOT.'
|
|
23
|
+
: ''));
|
|
24
|
+
}
|
|
25
|
+
if (runEnv !== undefined && selected.length === 0) {
|
|
26
|
+
throw new Error(`profiles.yaml has no profile with "env: ${runEnv}". Known environments: ${[...new Set(profiles.map((profile) => profile.env))].join(', ')}.`);
|
|
27
|
+
}
|
|
28
|
+
// Every path is absolute on purpose: a project that uses the framework re-exports this file from its own
|
|
29
|
+
// `playwright.config.ts`, and Playwright resolves relative paths against the file that holds them (this one).
|
|
30
|
+
// The compiled package has `summary-reporter.js`; the framework repository runs the `.ts` source.
|
|
31
|
+
const summaryReporter = [path.join(import.meta.dirname, 'lib', 'summary-reporter.js'), path.join(import.meta.dirname, 'lib', 'summary-reporter.ts')].find((file) => fs.existsSync(file));
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
testDir: path.join(ROOT, 'tests'),
|
|
34
|
+
outputDir: path.join(TMP_DIR, 'results'),
|
|
35
|
+
timeout: 60_000,
|
|
36
|
+
expect: {
|
|
37
|
+
timeout: 10_000,
|
|
38
|
+
toHaveScreenshot: { maxDiffPixelRatio: 0.01, animations: 'disabled' },
|
|
39
|
+
},
|
|
40
|
+
retries: 1,
|
|
41
|
+
// Be gentle with the sites under test.
|
|
42
|
+
workers: 2,
|
|
43
|
+
metadata: {
|
|
44
|
+
environment: runEnv ? runEnv.toUpperCase() : 'not set',
|
|
45
|
+
runId,
|
|
46
|
+
workspace: ROOT,
|
|
47
|
+
},
|
|
48
|
+
reporter: [
|
|
49
|
+
['list'],
|
|
50
|
+
[
|
|
51
|
+
'html',
|
|
52
|
+
{
|
|
53
|
+
outputFolder: path.join(REPORTS_DIR, runId, 'html'),
|
|
54
|
+
open: 'never',
|
|
55
|
+
title: runEnv ? `${PRODUCT_NAME} report - ${runEnv.toUpperCase()} - ${runId}` : `${PRODUCT_NAME} report - ${runId}`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
[summaryReporter],
|
|
59
|
+
],
|
|
60
|
+
use: {
|
|
61
|
+
// Project convention in uc-frontend: data-tn attributes.
|
|
62
|
+
testIdAttribute: 'data-tn',
|
|
63
|
+
// `pnpm asrun --step-text` overlays the current test.step() title on the video.
|
|
64
|
+
video: recording
|
|
65
|
+
? {
|
|
66
|
+
mode: 'on',
|
|
67
|
+
size: { width: 1440, height: 900 },
|
|
68
|
+
...(stepText && { show: { test: { level: 'step', position: 'top-left' } } }),
|
|
69
|
+
}
|
|
70
|
+
: 'off',
|
|
71
|
+
trace: recording ? 'on' : 'off',
|
|
72
|
+
screenshot: recording ? 'on' : 'off',
|
|
73
|
+
// Without these a locator that matches nothing (a dialog that never opened) waits for ever. In a test the 60 s
|
|
74
|
+
// test timeout ended it anyway; in an MCP exploration nothing did, and the agent hung.
|
|
75
|
+
actionTimeout: 15_000,
|
|
76
|
+
navigationTimeout: 30_000,
|
|
77
|
+
locale: 'en-US',
|
|
78
|
+
timezoneId: 'America/New_York',
|
|
79
|
+
},
|
|
80
|
+
// One project per profile. Baseline images are stored per project, so every environment keeps its own.
|
|
81
|
+
projects: selected.map((profile) => ({
|
|
82
|
+
name: profile.id,
|
|
83
|
+
// Retrying cannot fix a missing login.
|
|
84
|
+
...(notLoggedIn.has(profile.id) && { retries: 0 }),
|
|
85
|
+
use: {
|
|
86
|
+
...devices['Desktop Chrome'],
|
|
87
|
+
viewport: { width: 1440, height: 900 },
|
|
88
|
+
baseURL: profile.baseURL,
|
|
89
|
+
...(fs.existsSync(authFile(profile.id)) && { storageState: authFile(profile.id) }),
|
|
90
|
+
},
|
|
91
|
+
})),
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { COPIED_SCRIPTS } from '../lib/build-artifacts.js';
|
|
5
|
+
import { commandScript } from '../lib/node-cli.js';
|
|
6
|
+
import { FRAMEWORK_ROOT } from '../lib/paths.js';
|
|
7
|
+
// Compiles the framework to `dist/`: JavaScript and type declarations that Node and Playwright can load from a
|
|
8
|
+
// node_modules folder (Node refuses to strip types there). The plain `.mjs` tools are copied as they are.
|
|
9
|
+
const tsc = commandScript('typescript', 'tsc');
|
|
10
|
+
const result = spawnSync(process.execPath, [tsc, '-p', path.join(FRAMEWORK_ROOT, 'tsconfig.build.json')], {
|
|
11
|
+
cwd: FRAMEWORK_ROOT,
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
});
|
|
14
|
+
if ((result.status ?? 1) !== 0)
|
|
15
|
+
process.exit(result.status ?? 1);
|
|
16
|
+
for (const file of COPIED_SCRIPTS) {
|
|
17
|
+
const from = path.join(FRAMEWORK_ROOT, 'scripts', file);
|
|
18
|
+
const to = path.join(FRAMEWORK_ROOT, 'dist', 'scripts', file);
|
|
19
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
20
|
+
fs.copyFileSync(from, to);
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { CASES_DIR } from '../lib/paths.js';
|
|
3
|
+
import { listCaseFileIds, readCaseMeta } from '../lib/cases.js';
|
|
4
|
+
import { parseCaseDocument } from '../lib/case-doc.js';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { ROOT } from '../lib/paths.js';
|
|
7
|
+
import { checksNeedingTarget, parseTargets, readCaseText, REQUIRED_FIELDS, targetRefsIn } from '../lib/targets.js';
|
|
8
|
+
import { parseCaseValues } from '../lib/values.js';
|
|
9
|
+
/** Step numbers of the lines marked `[input]` or `[writes]` in "## Steps" (a person does them), and the numbers that have a `- Recipe:` line under them (or an old "## Inputs" line). */
|
|
10
|
+
function inputSteps(id) {
|
|
11
|
+
const doc = parseCaseDocument(fs.readFileSync(path.join(CASES_DIR, `${id}.md`), 'utf8'));
|
|
12
|
+
const marked = doc.steps
|
|
13
|
+
.filter((step) => step.tags.includes('input') || step.tags.includes('writes'))
|
|
14
|
+
.map((step) => step.number);
|
|
15
|
+
const recorded = [
|
|
16
|
+
...doc.steps.flatMap((step) => step.recipes.map(() => step.number)),
|
|
17
|
+
...doc.inputs.map((input) => input.step).filter((step) => step !== null),
|
|
18
|
+
];
|
|
19
|
+
return { marked, recorded };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Warnings about the question blocks and the recipe lines of a case: a block nobody can answer, an answer that is not
|
|
23
|
+
* the last line (so the agent cannot tell what it belongs to), a question with no text to keep, a nesting that Markdown
|
|
24
|
+
* will not render inside its step, and a recipe under a step that is not tagged (all `warn`: old cases keep working).
|
|
25
|
+
*/
|
|
26
|
+
function questionProblems(id) {
|
|
27
|
+
const doc = parseCaseDocument(fs.readFileSync(path.join(CASES_DIR, `${id}.md`), 'utf8'));
|
|
28
|
+
const warnings = [];
|
|
29
|
+
for (const question of doc.questions) {
|
|
30
|
+
if (!question.hasAnswerLine) {
|
|
31
|
+
warnings.push(`the question on line ${question.line} has no "✍️ Answer:" line; add one so it can be answered in place`);
|
|
32
|
+
}
|
|
33
|
+
else if (question.answerLine !== question.lastLine) {
|
|
34
|
+
warnings.push(`the "✍️ Answer:" line of the question on line ${question.line} is not the last line of its block`);
|
|
35
|
+
}
|
|
36
|
+
if (!question.parent) {
|
|
37
|
+
warnings.push(`the question on line ${question.line} has no text above it: accepting it would delete a claim nobody sees`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const step = doc.steps.find((candidate) => candidate.line === question.parent?.line);
|
|
41
|
+
if (step && question.indent < 4) {
|
|
42
|
+
warnings.push(`the question on line ${question.line} is indented by ${question.indent} space(s) under step ${step.number}; 4 renders it inside the step`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const step of doc.steps) {
|
|
46
|
+
if (step.recipes.length === 0)
|
|
47
|
+
continue;
|
|
48
|
+
if (!step.tags.includes('input') && !step.tags.includes('writes')) {
|
|
49
|
+
warnings.push(`step ${step.number} has a "- Recipe:" line but is not tagged [input] or [writes]`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const input of doc.inputs) {
|
|
53
|
+
if (input.step === null)
|
|
54
|
+
continue;
|
|
55
|
+
const step = doc.steps.find((candidate) => candidate.number === input.step);
|
|
56
|
+
if (!step) {
|
|
57
|
+
warnings.push(`"## Inputs" line ${input.line} (old format) belongs to step ${input.step}, which this case does not have`);
|
|
58
|
+
}
|
|
59
|
+
else if (!step.tags.includes('input') && !step.tags.includes('writes')) {
|
|
60
|
+
warnings.push(`"## Inputs" line ${input.line} (old format) belongs to step ${input.step}, which is not tagged [input] or [writes]`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return warnings;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Warnings for the old layout. `## Data` and `## Inputs` are deprecated: the one-sentence summary belongs in
|
|
67
|
+
* `writes-note`, the cleanup in steps, and a person's recipe under its step. Cases keep working, so this never fails.
|
|
68
|
+
*/
|
|
69
|
+
function formatProblems(id) {
|
|
70
|
+
const doc = parseCaseDocument(fs.readFileSync(path.join(CASES_DIR, `${id}.md`), 'utf8'));
|
|
71
|
+
const warnings = [];
|
|
72
|
+
if (doc.sections.some((section) => section.name === 'Data')) {
|
|
73
|
+
warnings.push('has a "## Data" section, which is no longer part of the case format: move it into "writes-note", the delete steps or "## Notes"');
|
|
74
|
+
}
|
|
75
|
+
if (doc.inputs.length > 0) {
|
|
76
|
+
warnings.push('uses the old "## Inputs" section: move each line under its step as "- Recipe (phase): ..."');
|
|
77
|
+
}
|
|
78
|
+
return warnings;
|
|
79
|
+
}
|
|
80
|
+
/** Problems with the target cards (`## Targets`) of a case: `fail` stops, `warn` is what `/explore-case` still has to settle. */
|
|
81
|
+
function targetProblems(id) {
|
|
82
|
+
const text = readCaseText(id);
|
|
83
|
+
const cards = parseTargets(text);
|
|
84
|
+
const fail = [];
|
|
85
|
+
const warn = [];
|
|
86
|
+
const known = new Set(cards.map((card) => card.id));
|
|
87
|
+
for (const card of cards) {
|
|
88
|
+
const missing = REQUIRED_FIELDS.filter((field) => !card.fields[field]);
|
|
89
|
+
if (missing.length > 0 && !card.blocking)
|
|
90
|
+
fail.push(`target ${card.id} "${card.name}" has no ${missing.map((field) => `"${field}"`).join(', ')}`);
|
|
91
|
+
for (const [label, file] of Object.entries(card.references)) {
|
|
92
|
+
if (/^none\b/i.test(file))
|
|
93
|
+
continue;
|
|
94
|
+
// References live in .explore/ (git-ignored, they show logged-in pages), so a fresh checkout has none: a warning.
|
|
95
|
+
if (!file.startsWith('.explore/'))
|
|
96
|
+
fail.push(`target ${card.id}: the reference image for ${label} must be saved under .explore/targets/<case-id>/ (got ${file})`);
|
|
97
|
+
else if (!fs.existsSync(path.join(ROOT, file)))
|
|
98
|
+
warn.push(`target ${card.id}: the reference image for ${label} is not on this machine: ${file}`);
|
|
99
|
+
}
|
|
100
|
+
if (Object.keys(card.references).length === 0 && !card.blocking) {
|
|
101
|
+
warn.push(`target ${card.id} "${card.name}" has no "Reference <profile-id>:" line (a highlighted screenshot of what the check looks at, or "none: <reason>")`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const body = text.replace(/^## Targets\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, '').replace(/^## Source\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, '');
|
|
105
|
+
for (const ref of targetRefsIn(body))
|
|
106
|
+
if (!known.has(ref))
|
|
107
|
+
fail.push(`a check refers to [${ref}] but "## Targets" has no card ${ref}`);
|
|
108
|
+
for (const check of checksNeedingTarget(text)) {
|
|
109
|
+
warn.push(`"${check.section}" has a check on an image, file or logo without a target card ([T<n>]): ${check.line.slice(0, 90)}`);
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const values = parseCaseValues(text, `cases/${id}.md`);
|
|
113
|
+
if (Object.keys(values).length === 0) {
|
|
114
|
+
warn.push('has no "## Values" block; a spec that asserts expected values cannot run for any profile');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
fail.push(error.message);
|
|
119
|
+
}
|
|
120
|
+
return { fail, warn };
|
|
121
|
+
}
|
|
122
|
+
const ids = process.argv.slice(2).length > 0 ? process.argv.slice(2) : listCaseFileIds();
|
|
123
|
+
let problems = 0;
|
|
124
|
+
for (const id of ids) {
|
|
125
|
+
try {
|
|
126
|
+
const meta = readCaseMeta(id);
|
|
127
|
+
console.log(`ok ${id} suites=[${meta.suites.join(', ')}] writes=[${meta.writes.join(', ')}]` +
|
|
128
|
+
`${meta.envs ? ` envs=[${meta.envs.join(', ')}]` : ''}`);
|
|
129
|
+
const targets = targetProblems(id);
|
|
130
|
+
for (const message of targets.warn)
|
|
131
|
+
console.log(`warn ${id}: ${message}`);
|
|
132
|
+
for (const message of targets.fail)
|
|
133
|
+
throw new Error(message);
|
|
134
|
+
const { marked, recorded } = inputSteps(id);
|
|
135
|
+
const missing = marked.filter((step) => !recorded.includes(step));
|
|
136
|
+
if (missing.length > 0) {
|
|
137
|
+
console.log(`warn ${id}: step ${missing.join(', ')} is marked [input] or [writes] but has no "- Recipe:" line under it (run /explore-case --attended)`);
|
|
138
|
+
}
|
|
139
|
+
for (const message of formatProblems(id))
|
|
140
|
+
console.log(`warn ${id}: ${message}`);
|
|
141
|
+
for (const message of questionProblems(id))
|
|
142
|
+
console.log(`warn ${id}: ${message}`);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
problems++;
|
|
146
|
+
console.error(`FAIL ${id}: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
process.exit(problems > 0 ? 1 : 0);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { listEnvs } from '../lib/profiles.js';
|
|
2
|
+
import { describeLeftovers, findLeftovers, forgetLedgers } from '../lib/records.js';
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const envArg = args.find((arg) => arg.startsWith('--env='))?.slice('--env='.length) ?? args[args.indexOf('--env') + 1];
|
|
5
|
+
const forget = args.includes('--forget');
|
|
6
|
+
if (!envArg || !listEnvs().includes(envArg)) {
|
|
7
|
+
console.error('Usage: pnpm cleanup --env <env> [--forget]\n' +
|
|
8
|
+
` --env is required (${listEnvs().join(' | ')}); there is no default.\n` +
|
|
9
|
+
' Lists test data that cases created and could not remove (from .tmp/records).\n' +
|
|
10
|
+
' Delete it by hand in the product, then run with --forget to clear the list.');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const leftovers = findLeftovers({ env: envArg });
|
|
14
|
+
if (leftovers.length === 0) {
|
|
15
|
+
console.log(`No test data left behind in ${envArg.toUpperCase()}.`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
console.log(`Test data left behind in ${envArg.toUpperCase()} (named "[asrun] ..."):\n${describeLeftovers(leftovers)}`);
|
|
19
|
+
if (forget) {
|
|
20
|
+
console.log(`\nForgot ${forgetLedgers(envArg)} ledger file(s). Only do this after the data is deleted.`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log('\nDelete these by hand in the product, then run: pnpm cleanup --env ' + envArg + ' --forget');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// `pnpm asrun <command>`: a thin dispatcher so every subcommand keeps one entry point.
|
|
2
|
+
// Anything that is not a subcommand is passed to the run command unchanged.
|
|
3
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
4
|
+
async function main() {
|
|
5
|
+
if (command === 'init') {
|
|
6
|
+
const { main: init } = await import('./init.js');
|
|
7
|
+
process.exitCode = await init(rest);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (command === 'doctor') {
|
|
11
|
+
const { main: doctor } = await import('./doctor.js');
|
|
12
|
+
process.exitCode = await doctor(rest);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (command === 'help' || command === '--help-init') {
|
|
16
|
+
console.log(USAGE);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
await import('./run.js');
|
|
20
|
+
}
|
|
21
|
+
const USAGE = `Usage: pnpm asrun [init|doctor] [options]
|
|
22
|
+
|
|
23
|
+
(no subcommand) Run the tests of one environment. See "pnpm asrun --help".
|
|
24
|
+
init Create the files a workspace needs (never overwrites; --update refreshes generated files,
|
|
25
|
+
--root <dir> targets another folder, --force <path> overwrites one file).
|
|
26
|
+
doctor Check the workspace: profiles, tsconfig alias, generated files, skill overrides.`;
|
|
27
|
+
await main();
|
|
28
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(args: string[]): Promise<number>;
|