@ttt-productions/repo-guards 0.1.22 → 0.1.23

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.
@@ -1 +1 @@
1
- {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/modules/import-paths/guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAS,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAKzE,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,UAAU,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1D;AAsCD,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAAC,eAAe,CAiCnE,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,eAAe,GACrB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAM3C"}
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/modules/import-paths/guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAS,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAKzE,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,UAAU,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1D;AAuCD,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAAC,eAAe,CAiCnE,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,eAAe,GACrB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAM3C"}
@@ -1,12 +1,12 @@
1
- import { isTestPath, isUnderRoot, kindOfFile } from '../../node/layout.js';
1
+ import { declaredRoots, isTestPath, isUnderRoot, kindOfFile } from '../../node/layout.js';
2
2
  import { importAliasesProblem, reviewedProblems } from '../../support/facts.js';
3
3
  import { analyzeImportPaths, createImportPathProfile, findUnusedIndexMembers } from './analyzer.js';
4
4
  import { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';
5
5
  const SCOPE = {
6
- roots: ['app', 'functions'],
6
+ roots: ['app', 'functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],
7
7
  kinds: ['code'],
8
8
  tests: true,
9
- optionalRoots: ['functions'],
9
+ optionalRoots: ['functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],
10
10
  };
11
11
  function readModules(files) {
12
12
  const modules = new Map();
@@ -15,8 +15,9 @@ function readModules(files) {
15
15
  return modules;
16
16
  }
17
17
  function analyzedRoots(reader) {
18
- const { app, functions, sharedSource } = reader.layout.roots;
19
- return [app.path, ...[functions, sharedSource].flatMap((root) => (root ? [root.path] : []))];
18
+ return declaredRoots(reader.layout)
19
+ .filter(({ role }) => role !== 'coreDist')
20
+ .map(({ root }) => root.path);
20
21
  }
21
22
  function excludedModuleExists(reader, p) {
22
23
  return ((reader.layout.exclusions ?? []).some((e) => isUnderRoot(p, e.path)) &&
@@ -1 +1 @@
1
- {"version":3,"file":"guard.js","sourceRoot":"","sources":["../../../src/modules/import-paths/guard.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding, GuardContext } from '../../guard.js';\nimport type { RepoReader, Scope } from '../../node/reader.js';\nimport { isTestPath, isUnderRoot, kindOfFile } from '../../node/layout.js';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport { importAliasesProblem, reviewedProblems } from '../../support/facts.js';\nimport { analyzeImportPaths, createImportPathProfile, findUnusedIndexMembers } from './analyzer.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\nexport interface ImportPathFacts {\n importAliases: Readonly<Record<string, string>>;\n exceptions: ReadonlyArray<{ file: string; why: string }>;\n}\n\nconst SCOPE: Scope = {\n roots: ['app', 'functions'],\n kinds: ['code'],\n tests: true,\n optionalRoots: ['functions'],\n};\n\nfunction readModules(files: ReturnType<RepoReader['files']>) {\n const modules = new Map<string, { text: string; sourceFile: import('typescript').SourceFile }>();\n for (const file of files)\n modules.set(file.path, { text: file.text, sourceFile: file.sourceFile });\n return modules;\n}\n\nfunction analyzedRoots(reader: RepoReader) {\n const { app, functions, sharedSource } = reader.layout.roots;\n return [app.path, ...[functions, sharedSource].flatMap((root) => (root ? [root.path] : []))];\n}\n\nfunction excludedModuleExists(reader: RepoReader, p: string): boolean {\n return (\n (reader.layout.exclusions ?? []).some((e) => isUnderRoot(p, e.path)) &&\n kindOfFile(p.slice(p.lastIndexOf('/') + 1)) === 'code' &&\n reader.exists(p)\n );\n}\n\nfunction profileFor(reader: RepoReader, facts: ImportPathFacts) {\n return createImportPathProfile({\n importAliases: { ...facts.importAliases },\n exceptions: facts.exceptions.map((e) => ({ ...e })),\n isTestFile: (p: string) => isTestPath(reader.layout, p),\n isExcludedModule: (p: string) => excludedModuleExists(reader, p),\n });\n}\n\nexport const importPathGuard: GenericGuardDefinition<ImportPathFacts> = {\n id: 'importPaths',\n absentWhen: false,\n factProblems(facts) {\n const problems: string[] = [];\n importAliasesProblem(facts?.importAliases, 'importAliases', problems);\n reviewedProblems(facts?.exceptions, 'exceptions', problems, { key: 'file' });\n return problems;\n },\n build(facts) {\n return [\n {\n title: 'every definition has exactly one import path',\n description:\n 'A module that is not a folder index never re-exports; a folder index forwards only files inside its folder; ' +\n 'code outside a folder that has an index imports through it, never past it; code inside a folder imports its ' +\n 'siblings directly, never through an index of a folder that contains it. Import the owner — the sibling file or the package.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context: GuardContext): Finding[] {\n const files = context.files(SCOPE);\n const result = analyzeImportPaths({\n files: readModules(files),\n roots: analyzedRoots(context.reader),\n profile: profileFor(context.reader, facts),\n });\n return result.violations.map((v) => ({\n file: v.file,\n detail: `${v.kind.replace(/-/g, ' ')}: ${v.detail}`,\n }));\n },\n },\n ];\n },\n};\n\nexport function unusedIndexMembers(\n reader: RepoReader,\n facts: ImportPathFacts,\n): Array<{ file: string; unused: string[] }> {\n return findUnusedIndexMembers({\n files: readModules(reader.files(SCOPE)),\n roots: analyzedRoots(reader),\n profile: profileFor(reader, facts),\n });\n}\n"]}
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../../../src/modules/import-paths/guard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAE1F,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAOxE,MAAM,KAAK,GAAU;IACnB,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC;IACrE,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,KAAK,EAAE,IAAI;IACX,aAAa,EAAE,CAAC,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC;CACvE,CAAC;AAEF,SAAS,WAAW,CAAC,KAAsC;IACzD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAyE,CAAC;IACjG,KAAK,MAAM,IAAI,IAAI,KAAK;QACtB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;IAC3E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,aAAa,CAAC,MAAkB;IACvC,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC;SAChC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,KAAK,UAAU,CAAC;SACzC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,oBAAoB,CAAC,MAAkB,EAAE,CAAS;IACzD,OAAO,CACL,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QACpE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,MAAM;QACtD,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CACjB,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,MAAkB,EAAE,KAAsB;IAC5D,OAAO,uBAAuB,CAAC;QAC7B,aAAa,EAAE,EAAE,GAAG,KAAK,CAAC,aAAa,EAAE;QACzC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QACnD,UAAU,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACvD,gBAAgB,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,oBAAoB,CAAC,MAAM,EAAE,CAAC,CAAC;KACjE,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAA4C;IACtE,EAAE,EAAE,aAAa;IACjB,UAAU,EAAE,KAAK;IACjB,YAAY,CAAC,KAAK;QAChB,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,oBAAoB,CAAC,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,QAAQ,CAAC,CAAC;QACtE,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QAC7E,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,KAAK,CAAC,KAAK;QACT,OAAO;YACL;gBACE,KAAK,EAAE,8CAA8C;gBACrD,WAAW,EACT,8GAA8G;oBAC9G,8GAA8G;oBAC9G,6HAA6H;gBAC/H,SAAS,EAAE,2BAA2B;gBACtC,GAAG,CAAC,OAAqB;oBACvB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBACnC,MAAM,MAAM,GAAG,kBAAkB,CAAC;wBAChC,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC;wBACzB,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC;wBACpC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC;qBAC3C,CAAC,CAAC;oBACH,OAAO,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;wBACnC,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE;qBACpD,CAAC,CAAC,CAAC;gBACN,CAAC;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,kBAAkB,CAChC,MAAkB,EAClB,KAAsB;IAEtB,OAAO,sBAAsB,CAAC;QAC5B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACvC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC;QAC5B,OAAO,EAAE,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC;KACnC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { Finding, GuardContext } from '../../guard.js';\nimport type { RepoReader, Scope } from '../../node/reader.js';\nimport { declaredRoots, isTestPath, isUnderRoot, kindOfFile } from '../../node/layout.js';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport { importAliasesProblem, reviewedProblems } from '../../support/facts.js';\nimport { analyzeImportPaths, createImportPathProfile, findUnusedIndexMembers } from './analyzer.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\nexport interface ImportPathFacts {\n importAliases: Readonly<Record<string, string>>;\n exceptions: ReadonlyArray<{ file: string; why: string }>;\n}\n\nconst SCOPE: Scope = {\n roots: ['app', 'functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],\n kinds: ['code'],\n tests: true,\n optionalRoots: ['functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],\n};\n\nfunction readModules(files: ReturnType<RepoReader['files']>) {\n const modules = new Map<string, { text: string; sourceFile: import('typescript').SourceFile }>();\n for (const file of files)\n modules.set(file.path, { text: file.text, sourceFile: file.sourceFile });\n return modules;\n}\n\nfunction analyzedRoots(reader: RepoReader) {\n return declaredRoots(reader.layout)\n .filter(({ role }) => role !== 'coreDist')\n .map(({ root }) => root.path);\n}\n\nfunction excludedModuleExists(reader: RepoReader, p: string): boolean {\n return (\n (reader.layout.exclusions ?? []).some((e) => isUnderRoot(p, e.path)) &&\n kindOfFile(p.slice(p.lastIndexOf('/') + 1)) === 'code' &&\n reader.exists(p)\n );\n}\n\nfunction profileFor(reader: RepoReader, facts: ImportPathFacts) {\n return createImportPathProfile({\n importAliases: { ...facts.importAliases },\n exceptions: facts.exceptions.map((e) => ({ ...e })),\n isTestFile: (p: string) => isTestPath(reader.layout, p),\n isExcludedModule: (p: string) => excludedModuleExists(reader, p),\n });\n}\n\nexport const importPathGuard: GenericGuardDefinition<ImportPathFacts> = {\n id: 'importPaths',\n absentWhen: false,\n factProblems(facts) {\n const problems: string[] = [];\n importAliasesProblem(facts?.importAliases, 'importAliases', problems);\n reviewedProblems(facts?.exceptions, 'exceptions', problems, { key: 'file' });\n return problems;\n },\n build(facts) {\n return [\n {\n title: 'every definition has exactly one import path',\n description:\n 'A module that is not a folder index never re-exports; a folder index forwards only files inside its folder; ' +\n 'code outside a folder that has an index imports through it, never past it; code inside a folder imports its ' +\n 'siblings directly, never through an index of a folder that contains it. Import the owner — the sibling file or the package.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context: GuardContext): Finding[] {\n const files = context.files(SCOPE);\n const result = analyzeImportPaths({\n files: readModules(files),\n roots: analyzedRoots(context.reader),\n profile: profileFor(context.reader, facts),\n });\n return result.violations.map((v) => ({\n file: v.file,\n detail: `${v.kind.replace(/-/g, ' ')}: ${v.detail}`,\n }));\n },\n },\n ];\n },\n};\n\nexport function unusedIndexMembers(\n reader: RepoReader,\n facts: ImportPathFacts,\n): Array<{ file: string; unused: string[] }> {\n return findUnusedIndexMembers({\n files: readModules(reader.files(SCOPE)),\n roots: analyzedRoots(reader),\n profile: profileFor(reader, facts),\n });\n}\n"]}
@@ -1,8 +1,13 @@
1
+ import ts from 'typescript';
1
2
  import type { GenericGuardDefinition } from '../../node/suite/config.js';
2
3
  import { type FileView } from '../../reader/view.js';
3
4
  import { type AnchoredEntry, type Occurrence } from '../../support/anchored-review.js';
4
- export declare function retriedActions(source: FileView | string, helpers?: readonly string[]): Occurrence[];
5
- export declare function swallowedActions(source: FileView | string, helpers?: readonly string[]): Occurrence[];
5
+ export interface ActionReach {
6
+ actionIn(file: FileView, root: ts.Node): string[] | undefined;
7
+ }
8
+ export declare function createActionReach(files: readonly FileView[], helpers?: readonly string[]): ActionReach;
9
+ export declare function retriedActions(source: FileView | string, helpers?: readonly string[], reach?: ActionReach): Occurrence[];
10
+ export declare function swallowedActions(source: FileView | string, helpers?: readonly string[], reach?: ActionReach): Occurrence[];
6
11
  export interface E2eActionFacts {
7
12
  actionHelpers: readonly string[];
8
13
  reviewed: readonly AnchoredEntry[];
@@ -1 +1 @@
1
- {"version":3,"file":"e2e-actions.d.ts","sourceRoot":"","sources":["../../../src/modules/repo/e2e-actions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAuE1C,wBAAgB,cAAc,CAC5B,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,GAAE,SAAS,MAAM,EAAO,GAC9B,UAAU,EAAE,CAsBd;AAwBD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,GAAE,SAAS,MAAM,EAAO,GAC9B,UAAU,EAAE,CAkDd;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CACpC;AAUD,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,cAAc,CA4BvE,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,cAAc,CA4BvE,CAAC"}
1
+ {"version":3,"file":"e2e-actions.d.ts","sourceRoot":"","sources":["../../../src/modules/repo/e2e-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGjE,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAmI1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;CAC/D;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,OAAO,GAAE,SAAS,MAAM,EAAO,GAC9B,WAAW,CAgDb;AAcD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,GAAE,SAAS,MAAM,EAAO,EAC/B,KAAK,CAAC,EAAE,WAAW,GAClB,UAAU,EAAE,CA4Bd;AAmBD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,GAAE,SAAS,MAAM,EAAO,EAC/B,KAAK,CAAC,EAAE,WAAW,GAClB,UAAU,EAAE,CA8Cd;AAWD,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CACpC;AAUD,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,cAAc,CA8BvE,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,cAAc,CA6BvE,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import ts from 'typescript';
2
2
  import { viewOfText } from '../../reader/view.js';
3
3
  import { calleeName, forEachNode } from '../../support/ast.js';
4
+ import { resolveRelative } from '../../support/paths.js';
4
5
  import { anchoredFindings, anchoredProblems, } from '../../support/anchored-review.js';
5
6
  import { namesProblem, objectProblem } from '../../support/facts.js';
6
7
  import { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';
@@ -66,29 +67,141 @@ function retriedCallbacks(sf) {
66
67
  });
67
68
  return callbacks;
68
69
  }
69
- export function retriedActions(source, helpers = []) {
70
- const file = typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;
71
- const sf = file.sourceFile;
70
+ const isRepeatingLoop = (node) => ts.isForStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node);
71
+ function functionBodiesOf(sf) {
72
+ const bodies = new Map();
73
+ const add = (name, body) => bodies.set(name, [...(bodies.get(name) ?? []), body]);
74
+ forEachNode(sf, (node) => {
75
+ if (ts.isFunctionDeclaration(node) && node.name && node.body)
76
+ add(node.name.text, node.body);
77
+ if (ts.isVariableDeclaration(node) &&
78
+ ts.isIdentifier(node.name) &&
79
+ node.initializer &&
80
+ isFunctionNode(node.initializer))
81
+ add(node.name.text, node.initializer.body);
82
+ });
83
+ return bodies;
84
+ }
85
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
86
+ function moduleCandidates(base) {
87
+ const stem = base.replace(/\.(?:[cm]?js|jsx)$/, '');
88
+ return [
89
+ base,
90
+ ...SOURCE_EXTENSIONS.map((ext) => stem + ext),
91
+ ...SOURCE_EXTENSIONS.map((ext) => `${base}/index${ext}`),
92
+ ];
93
+ }
94
+ // Where an imported function lives: the file and the name it is declared under there.
95
+ function namedImportsOf(file, paths) {
96
+ const imports = new Map();
97
+ for (const statement of file.sourceFile.statements) {
98
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
99
+ continue;
100
+ const bindings = statement.importClause?.namedBindings;
101
+ if (!bindings || !ts.isNamedImports(bindings))
102
+ continue;
103
+ const base = resolveRelative(file.path, statement.moduleSpecifier.text)?.path;
104
+ const target = base && moduleCandidates(base).find((candidate) => paths.has(candidate));
105
+ if (!target)
106
+ continue;
107
+ for (const element of bindings.elements)
108
+ imports.set(element.name.text, {
109
+ path: target,
110
+ name: (element.propertyName ?? element.name).text,
111
+ });
112
+ }
113
+ return imports;
114
+ }
115
+ export function createActionReach(files, helpers = []) {
72
116
  const actions = actionNamesOf(helpers);
73
- return retriedCallbacks(sf).flatMap((fn) => {
74
- let action;
75
- forEachNode(fn.body, (node) => {
76
- if (!action && ts.isCallExpression(node)) {
77
- const name = calleeName(node);
78
- if (name && actions.has(name))
79
- action = name;
117
+ const byPath = new Map(files.map((file) => [file.path, file]));
118
+ const paths = new Set(byPath.keys());
119
+ const bodiesCache = new Map();
120
+ const importsCache = new Map();
121
+ const bodiesOf = (file) => {
122
+ let bodies = bodiesCache.get(file.path);
123
+ if (!bodies)
124
+ bodiesCache.set(file.path, (bodies = functionBodiesOf(file.sourceFile)));
125
+ return bodies;
126
+ };
127
+ const importsOf = (file) => {
128
+ let imports = importsCache.get(file.path);
129
+ if (!imports)
130
+ importsCache.set(file.path, (imports = namedImportsOf(file, paths)));
131
+ return imports;
132
+ };
133
+ function search(file, root, seen) {
134
+ let found;
135
+ forEachNode(root, (node) => {
136
+ if (found || !ts.isCallExpression(node))
137
+ return;
138
+ const name = calleeName(node);
139
+ if (!name)
140
+ return;
141
+ if (actions.has(name)) {
142
+ found = [name];
143
+ return;
144
+ }
145
+ if (!ts.isIdentifier(node.expression))
146
+ return;
147
+ const imported = importsOf(file).get(name);
148
+ const local = bodiesOf(file).get(name);
149
+ const owner = local ? file : imported ? byPath.get(imported.path) : undefined;
150
+ const declared = local ? name : imported?.name;
151
+ if (!owner || !declared)
152
+ return;
153
+ const key = `${owner.path}#${declared}`;
154
+ if (seen.has(key))
155
+ return;
156
+ seen.add(key);
157
+ for (const body of bodiesOf(owner).get(declared) ?? []) {
158
+ const inner = search(owner, body, seen);
159
+ if (inner) {
160
+ found = [name, ...inner];
161
+ return;
162
+ }
80
163
  }
81
164
  });
82
- return action
83
- ? [
84
- {
85
- start: fn.getStart(sf),
86
- end: fn.getEnd(),
87
- detail: `a retry loop repeats ${action}() until it passes`,
88
- },
89
- ]
90
- : [];
165
+ return found;
166
+ }
167
+ return { actionIn: (file, root) => search(file, root, new Set()) };
168
+ }
169
+ // `['openWork', 'goto']` reads "goto() through openWork".
170
+ function describeAction(chain) {
171
+ const action = `${chain[chain.length - 1]}()`;
172
+ return chain.length > 1 ? `${action} through ${chain.slice(0, -1).join(' → ')}` : action;
173
+ }
174
+ const within = (inner, outer) => inner !== outer && inner.pos >= outer.pos && inner.end <= outer.end;
175
+ const viewOf = (source) => typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;
176
+ export function retriedActions(source, helpers = [], reach) {
177
+ const file = viewOf(source);
178
+ const sf = file.sourceFile;
179
+ const reachFrom = reach ?? createActionReach([file], helpers);
180
+ const loops = [];
181
+ forEachNode(sf, (node) => {
182
+ if (isRepeatingLoop(node))
183
+ loops.push(node);
184
+ });
185
+ const acting = [
186
+ ...retriedCallbacks(sf).map((node) => ({
187
+ node,
188
+ body: node.body,
189
+ kind: 'a retry loop',
190
+ tail: ' until it passes',
191
+ })),
192
+ ...loops.map((node) => ({ node, body: node, kind: 'a for / while / do loop', tail: '' })),
193
+ ].flatMap((loop) => {
194
+ const chain = reachFrom.actionIn(file, loop.body);
195
+ return chain ? [{ ...loop, chain }] : [];
91
196
  });
197
+ return acting
198
+ .filter((loop) => !acting.some((outer) => within(loop.node, outer.node)))
199
+ .sort((a, b) => a.node.getStart(sf) - b.node.getStart(sf))
200
+ .map((loop) => ({
201
+ start: loop.node.getStart(sf),
202
+ end: loop.node.getEnd(),
203
+ detail: `${loop.kind} repeats ${describeAction(loop.chain)}${loop.tail}`,
204
+ }));
92
205
  }
93
206
  function catchesRejection(handler) {
94
207
  if (!isFunctionNode(handler))
@@ -100,21 +213,18 @@ function catchesRejection(handler) {
100
213
  });
101
214
  return !rethrows;
102
215
  }
103
- // The call a `.catch()` is chained onto: `x.click().catch(…)` → `click`.
104
- function caughtCallOf(catchCall, names) {
216
+ // The call a `.catch()` is chained onto: `x.click().catch(…)` → the `click()` call.
217
+ function caughtCall(catchCall) {
105
218
  if (!ts.isPropertyAccessExpression(catchCall.expression))
106
219
  return undefined;
107
220
  const receiver = catchCall.expression.expression;
108
221
  const call = ts.isParenthesizedExpression(receiver) ? receiver.expression : receiver;
109
- if (!ts.isCallExpression(call))
110
- return undefined;
111
- const name = calleeName(call);
112
- return name && names.has(name) ? name : undefined;
222
+ return ts.isCallExpression(call) ? call : undefined;
113
223
  }
114
- export function swallowedActions(source, helpers = []) {
115
- const file = typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;
224
+ export function swallowedActions(source, helpers = [], reach) {
225
+ const file = viewOf(source);
116
226
  const sf = file.sourceFile;
117
- const actions = actionNamesOf(helpers);
227
+ const reachFrom = reach ?? createActionReach([file], helpers);
118
228
  const hits = [];
119
229
  forEachNode(sf, (node) => {
120
230
  if (ts.isCallExpression(node) &&
@@ -122,19 +232,22 @@ export function swallowedActions(source, helpers = []) {
122
232
  node.expression.name.text === 'catch' &&
123
233
  node.arguments.length === 1 &&
124
234
  catchesRejection(node.arguments[0])) {
125
- const action = caughtCallOf(node, actions);
126
- const probe = caughtCallOf(node, STATE_PROBES);
127
- if (action)
235
+ const call = caughtCall(node);
236
+ if (!call)
237
+ return;
238
+ const name = calleeName(call);
239
+ const chain = reachFrom.actionIn(file, call);
240
+ if (chain)
128
241
  hits.push({
129
242
  start: node.getStart(sf),
130
243
  end: node.getEnd(),
131
- detail: `a .catch() swallows a failed ${action}()`,
244
+ detail: `a .catch() swallows a failed ${describeAction(chain)}`,
132
245
  });
133
- else if (probe)
246
+ else if (name && STATE_PROBES.has(name))
134
247
  hits.push({
135
248
  start: node.getStart(sf),
136
249
  end: node.getEnd(),
137
- detail: `a .catch() turns a failed ${probe}() — an ambiguous locator included — into a value`,
250
+ detail: `a .catch() turns a failed ${name}() — an ambiguous locator included — into a value`,
138
251
  });
139
252
  }
140
253
  if (ts.isTryStatement(node) && node.catchClause) {
@@ -145,24 +258,21 @@ export function swallowedActions(source, helpers = []) {
145
258
  });
146
259
  if (rethrows)
147
260
  return;
148
- let action;
149
- forEachNode(node.tryBlock, (inner) => {
150
- if (!action && ts.isCallExpression(inner)) {
151
- const name = calleeName(inner);
152
- if (name && actions.has(name))
153
- action = name;
154
- }
155
- });
156
- if (action)
261
+ const chain = reachFrom.actionIn(file, node.tryBlock);
262
+ if (chain)
157
263
  hits.push({
158
264
  start: node.getStart(sf),
159
265
  end: node.getEnd(),
160
- detail: `a try/catch swallows a failed ${action}()`,
266
+ detail: `a try/catch swallows a failed ${describeAction(chain)}`,
161
267
  });
162
268
  }
163
269
  });
164
270
  return hits;
165
271
  }
272
+ function withReach(files, helpers, find) {
273
+ const reach = createActionReach(files, helpers);
274
+ return files.map((file) => ({ file, hits: find(file, reach) }));
275
+ }
166
276
  function e2eActionProblems(facts) {
167
277
  const problems = [];
168
278
  if (!objectProblem(facts, 'the facts', problems))
@@ -180,16 +290,16 @@ export const retryLoopActionGuard = {
180
290
  return [
181
291
  {
182
292
  title: 'no end-to-end retry loop repeats a user action',
183
- description: 'A toPass() or expect.poll() callback that clicks, types, or navigates repeats the action until it ' +
184
- 'happens to work, so the failure a real user would hit passes as green (RULE-STD-QUALITY-003). Act once, ' +
293
+ description: 'A toPass() or expect.poll() callback, or a for (;;) / while / do loop, that clicks, types, or navigates ' +
294
+ 'repeats the action until it happens to work, so the failure a real user would hit passes as green ' +
295
+ '(RULE-STD-QUALITY-003). Walking a list is written for…of, which is not a repeat. Act once, ' +
185
296
  'then assert with a web-first assertion; a loop that only re-reads state is fine. The actions are ' +
186
- "Playwright's input and navigation calls plus the app's declared actionHelpers. A loop that must act — " +
297
+ "Playwright's input and navigation calls plus the app's declared actionHelpers, reached directly or through " +
298
+ 'a plain call to a function the suite declares in the same file or imports from its own files. A loop that must act — ' +
187
299
  'polling a background result by reloading — is a reviewed entry whose anchor names that one loop, with ' +
188
300
  'its why; an entry that no longer matches a loop is stale.',
189
301
  timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,
190
- run: (context) => anchoredFindings(context
191
- .files(E2E_SCOPE)
192
- .map((file) => ({ file, hits: retriedActions(file, facts.actionHelpers) })), facts.reviewed, 'its anchor names no retry loop that acts'),
302
+ run: (context) => anchoredFindings(withReach(context.files(E2E_SCOPE), facts.actionHelpers, (file, reach) => retriedActions(file, facts.actionHelpers, reach)), facts.reviewed, 'its anchor names no retry loop that acts'),
193
303
  },
194
304
  ];
195
305
  },
@@ -203,16 +313,15 @@ export const swallowedActionGuard = {
203
313
  return [
204
314
  {
205
315
  title: 'no end-to-end action or state read swallows its failure',
206
- description: 'A .catch() or a try/catch that discards a failed click, fill, or navigation lets the next step run on a ' +
316
+ description: 'A .catch() or a try/catch that discards a failed click, fill, or navigation — made directly or through a plain ' +
317
+ "call to one of the suite's own functions — lets the next step run on a " +
207
318
  'page the action never reached, and one on isVisible(), isEnabled(), or isChecked() turns an ambiguous ' +
208
319
  '(strict-mode) locator into a silent "no". Let the failure throw: an action whose outcome is uncertain ' +
209
320
  "goes through the suite's outcome-verified helper, and a branch reads state with no catch. A catch that " +
210
321
  'must stay is a reviewed entry whose anchor names that one occurrence, with its why; an entry that no ' +
211
322
  'longer matches one is stale.',
212
323
  timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,
213
- run: (context) => anchoredFindings(context
214
- .files(E2E_SCOPE)
215
- .map((file) => ({ file, hits: swallowedActions(file, facts.actionHelpers) })), facts.reviewed, 'its anchor names no swallowed action or state read'),
324
+ run: (context) => anchoredFindings(withReach(context.files(E2E_SCOPE), facts.actionHelpers, (file, reach) => swallowedActions(file, facts.actionHelpers, reach)), facts.reviewed, 'its anchor names no swallowed action or state read'),
216
325
  },
217
326
  ];
218
327
  },
@@ -1 +1 @@
1
- {"version":3,"file":"e2e-actions.js","sourceRoot":"","sources":["../../../src/modules/repo/e2e-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAG5B,OAAO,EAAE,UAAU,EAAiB,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EACL,gBAAgB,EAChB,gBAAgB,GAGjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAExE,wFAAwF;AACxF,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC;IACtD,OAAO;IACP,OAAO;IACP,OAAO;IACP,UAAU;IACV,eAAe;IACf,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,WAAW;IACX,MAAM;IACN,OAAO;IACP,OAAO;IACP,mBAAmB;IACnB,QAAQ;IACR,cAAc;IACd,YAAY;IACZ,YAAY;IACZ,eAAe;IACf,KAAK;IACL,MAAM;IACN,SAAS;CACV,CAAC,CAAC;AAEH,gGAAgG;AAChG,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAC;IAChD,WAAW;IACX,YAAY;IACZ,YAAY;IACZ,WAAW;IACX,UAAU;IACV,WAAW;CACZ,CAAC,CAAC;AAEH,MAAM,SAAS,GAAU;IACvB,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC;IAC3B,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,KAAK,EAAE,IAAI;IACX,aAAa,EAAE,CAAC,WAAW,CAAC;CAC7B,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,OAA0B,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,kBAAkB,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;AAEnG,MAAM,cAAc,GAAG,CAAC,IAAa,EAAoD,EAAE,CACzF,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;AAE5D,2EAA2E;AAC3E,SAAS,gBAAgB,CAAC,EAAiB;IACzC,MAAM,SAAS,GAAoD,EAAE,CAAC;IACtE,WAAW,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;QACvB,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC;YAAE,OAAO;QAC1F,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAC1C,IAAI,MAAM,KAAK,QAAQ,IAAI,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;YACvD,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAC/B,IAAI,EAAE,IAAI,cAAc,CAAC,EAAE,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAClF,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvB,CAAC;QACD,IAAI,MAAM,KAAK,MAAM,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,MAAM,EAAE,CAAC;YACrD,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,EAAE,IAAI,cAAc,CAAC,EAAE,CAAC;gBAAE,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnD,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAyB,EACzB,UAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxF,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC;IAC3B,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACvC,OAAO,gBAAgB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QACzC,IAAI,MAA0B,CAAC;QAC/B,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;YAC5B,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;gBAC9B,IAAI,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,MAAM,GAAG,IAAI,CAAC;YAC/C,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,MAAM;YACX,CAAC,CAAC;gBACE;oBACE,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACtB,GAAG,EAAE,EAAE,CAAC,MAAM,EAAE;oBAChB,MAAM,EAAE,wBAAwB,MAAM,oBAAoB;iBAC3D;aACF;YACH,CAAC,CAAC,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CAAC,OAAsB;IAC9C,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;QACjC,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC;YAAE,QAAQ,GAAG,IAAI,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,QAAQ,CAAC;AACnB,CAAC;AAED,yEAAyE;AACzE,SAAS,YAAY,CACnB,SAA4B,EAC5B,KAA0B;IAE1B,IAAI,CAAC,EAAE,CAAC,0BAA0B,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3E,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC;IACjD,MAAM,IAAI,GAAG,EAAE,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC;IACrF,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACjD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAyB,EACzB,UAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxF,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC;IAC3B,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,IAAI,GAAiB,EAAE,CAAC;IAC9B,WAAW,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;QACvB,IACE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC;YACzB,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC;YAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO;YACrC,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;YAC3B,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EACnC,CAAC;YACD,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC3C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;YAC/C,IAAI,MAAM;gBACR,IAAI,CAAC,IAAI,CAAC;oBACR,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACxB,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE;oBAClB,MAAM,EAAE,gCAAgC,MAAM,IAAI;iBACnD,CAAC,CAAC;iBACA,IAAI,KAAK;gBACZ,IAAI,CAAC,IAAI,CAAC;oBACR,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACxB,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE;oBAClB,MAAM,EAAE,6BAA6B,KAAK,mDAAmD;iBAC9F,CAAC,CAAC;QACP,CAAC;QACD,IAAI,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAChD,IAAI,QAAQ,GAAG,KAAK,CAAC;YACrB,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC5C,IAAI,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC;oBAAE,QAAQ,GAAG,IAAI,CAAC;YAClD,CAAC,CAAC,CAAC;YACH,IAAI,QAAQ;gBAAE,OAAO;YACrB,IAAI,MAA0B,CAAC;YAC/B,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;gBACnC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;oBAC/B,IAAI,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;wBAAE,MAAM,GAAG,IAAI,CAAC;gBAC/C,CAAC;YACH,CAAC,CAAC,CAAC;YACH,IAAI,MAAM;gBACR,IAAI,CAAC,IAAI,CAAC;oBACR,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACxB,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE;oBAClB,MAAM,EAAE,iCAAiC,MAAM,IAAI;iBACpD,CAAC,CAAC;QACP,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC;AAOD,SAAS,iBAAiB,CAAC,KAAqB;IAC9C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAClE,YAAY,CAAC,KAAK,CAAC,aAAa,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IAClF,gBAAgB,CAAC,KAAK,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACvD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAA2C;IAC1E,EAAE,EAAE,kBAAkB;IACtB,UAAU,EAAE,wCAAwC;IACpD,qBAAqB,EAAE,KAAK;IAC5B,YAAY,EAAE,iBAAiB;IAC/B,KAAK,CAAC,KAAK;QACT,OAAO;YACL;gBACE,KAAK,EAAE,gDAAgD;gBACvD,WAAW,EACT,oGAAoG;oBACpG,0GAA0G;oBAC1G,mGAAmG;oBACnG,wGAAwG;oBACxG,wGAAwG;oBACxG,2DAA2D;gBAC7D,SAAS,EAAE,2BAA2B;gBACtC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,CACf,gBAAgB,CACd,OAAO;qBACJ,KAAK,CAAC,SAAS,CAAC;qBAChB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,EAC7E,KAAK,CAAC,QAAQ,EACd,0CAA0C,CAC3C;aACJ;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAA2C;IAC1E,EAAE,EAAE,qBAAqB;IACzB,UAAU,EAAE,wCAAwC;IACpD,qBAAqB,EAAE,KAAK;IAC5B,YAAY,EAAE,iBAAiB;IAC/B,KAAK,CAAC,KAAK;QACT,OAAO;YACL;gBACE,KAAK,EAAE,yDAAyD;gBAChE,WAAW,EACT,0GAA0G;oBAC1G,wGAAwG;oBACxG,wGAAwG;oBACxG,yGAAyG;oBACzG,uGAAuG;oBACvG,8BAA8B;gBAChC,SAAS,EAAE,2BAA2B;gBACtC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,CACf,gBAAgB,CACd,OAAO;qBACJ,KAAK,CAAC,SAAS,CAAC;qBAChB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,EAC/E,KAAK,CAAC,QAAQ,EACd,oDAAoD,CACrD;aACJ;SACF,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Scope } from '../../node/reader.js';\nimport { viewOfText, type FileView } from '../../reader/view.js';\nimport { calleeName, forEachNode } from '../../support/ast.js';\nimport {\n anchoredFindings,\n anchoredProblems,\n type AnchoredEntry,\n type Occurrence,\n} from '../../support/anchored-review.js';\nimport { namesProblem, objectProblem } from '../../support/facts.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\n// Playwright's locator and page operations that act on the page: input, and navigation.\nconst PLAYWRIGHT_ACTIONS: ReadonlySet<string> = new Set([\n 'check',\n 'clear',\n 'click',\n 'dblclick',\n 'dispatchEvent',\n 'dragTo',\n 'fill',\n 'goBack',\n 'goForward',\n 'goto',\n 'hover',\n 'press',\n 'pressSequentially',\n 'reload',\n 'selectOption',\n 'selectText',\n 'setChecked',\n 'setInputFiles',\n 'tap',\n 'type',\n 'uncheck',\n]);\n\n// Playwright's state reads: strict locators throw when a locator matches more than one element.\nconst STATE_PROBES: ReadonlySet<string> = new Set([\n 'isChecked',\n 'isDisabled',\n 'isEditable',\n 'isEnabled',\n 'isHidden',\n 'isVisible',\n]);\n\nconst E2E_SCOPE: Scope = {\n roots: ['e2e', 'hostedE2e'],\n kinds: ['code'],\n tests: true,\n optionalRoots: ['hostedE2e'],\n};\n\nconst actionNamesOf = (helpers: readonly string[]) => new Set([...PLAYWRIGHT_ACTIONS, ...helpers]);\n\nconst isFunctionNode = (node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression =>\n ts.isArrowFunction(node) || ts.isFunctionExpression(node);\n\n// `expect(fn).toPass(…)` and `expect.poll(fn)` retry `fn` until it passes.\nfunction retriedCallbacks(sf: ts.SourceFile): Array<ts.ArrowFunction | ts.FunctionExpression> {\n const callbacks: Array<ts.ArrowFunction | ts.FunctionExpression> = [];\n forEachNode(sf, (node) => {\n if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return;\n const method = node.expression.name.text;\n const target = node.expression.expression;\n if (method === 'toPass' && ts.isCallExpression(target)) {\n const fn = target.arguments[0];\n if (fn && isFunctionNode(fn) && calleeName(target)?.toLowerCase().endsWith('expect'))\n callbacks.push(fn);\n }\n if (method === 'poll' && calleeName(node) === 'poll') {\n const fn = node.arguments[0];\n if (fn && isFunctionNode(fn)) callbacks.push(fn);\n }\n });\n return callbacks;\n}\n\nexport function retriedActions(\n source: FileView | string,\n helpers: readonly string[] = [],\n): Occurrence[] {\n const file = typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;\n const sf = file.sourceFile;\n const actions = actionNamesOf(helpers);\n return retriedCallbacks(sf).flatMap((fn) => {\n let action: string | undefined;\n forEachNode(fn.body, (node) => {\n if (!action && ts.isCallExpression(node)) {\n const name = calleeName(node);\n if (name && actions.has(name)) action = name;\n }\n });\n return action\n ? [\n {\n start: fn.getStart(sf),\n end: fn.getEnd(),\n detail: `a retry loop repeats ${action}() until it passes`,\n },\n ]\n : [];\n });\n}\n\nfunction catchesRejection(handler: ts.Expression): boolean {\n if (!isFunctionNode(handler)) return true;\n let rethrows = false;\n forEachNode(handler.body, (node) => {\n if (ts.isThrowStatement(node)) rethrows = true;\n });\n return !rethrows;\n}\n\n// The call a `.catch()` is chained onto: `x.click().catch(…)` → `click`.\nfunction caughtCallOf(\n catchCall: ts.CallExpression,\n names: ReadonlySet<string>,\n): string | undefined {\n if (!ts.isPropertyAccessExpression(catchCall.expression)) return undefined;\n const receiver = catchCall.expression.expression;\n const call = ts.isParenthesizedExpression(receiver) ? receiver.expression : receiver;\n if (!ts.isCallExpression(call)) return undefined;\n const name = calleeName(call);\n return name && names.has(name) ? name : undefined;\n}\n\nexport function swallowedActions(\n source: FileView | string,\n helpers: readonly string[] = [],\n): Occurrence[] {\n const file = typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;\n const sf = file.sourceFile;\n const actions = actionNamesOf(helpers);\n const hits: Occurrence[] = [];\n forEachNode(sf, (node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'catch' &&\n node.arguments.length === 1 &&\n catchesRejection(node.arguments[0])\n ) {\n const action = caughtCallOf(node, actions);\n const probe = caughtCallOf(node, STATE_PROBES);\n if (action)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a .catch() swallows a failed ${action}()`,\n });\n else if (probe)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a .catch() turns a failed ${probe}() — an ambiguous locator included — into a value`,\n });\n }\n if (ts.isTryStatement(node) && node.catchClause) {\n let rethrows = false;\n forEachNode(node.catchClause.block, (inner) => {\n if (ts.isThrowStatement(inner)) rethrows = true;\n });\n if (rethrows) return;\n let action: string | undefined;\n forEachNode(node.tryBlock, (inner) => {\n if (!action && ts.isCallExpression(inner)) {\n const name = calleeName(inner);\n if (name && actions.has(name)) action = name;\n }\n });\n if (action)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a try/catch swallows a failed ${action}()`,\n });\n }\n });\n return hits;\n}\n\nexport interface E2eActionFacts {\n actionHelpers: readonly string[];\n reviewed: readonly AnchoredEntry[];\n}\n\nfunction e2eActionProblems(facts: E2eActionFacts): string[] {\n const problems: string[] = [];\n if (!objectProblem(facts, 'the facts', problems)) return problems;\n namesProblem(facts.actionHelpers, 'actionHelpers', problems, { nonEmpty: false });\n anchoredProblems(facts.reviewed, 'reviewed', problems);\n return problems;\n}\n\nexport const retryLoopActionGuard: GenericGuardDefinition<E2eActionFacts> = {\n id: 'retryLoopActions',\n absentWhen: 'the repository has no end-to-end suite',\n absentOnlyWithoutRoot: 'e2e',\n factProblems: e2eActionProblems,\n build(facts) {\n return [\n {\n title: 'no end-to-end retry loop repeats a user action',\n description:\n 'A toPass() or expect.poll() callback that clicks, types, or navigates repeats the action until it ' +\n 'happens to work, so the failure a real user would hit passes as green (RULE-STD-QUALITY-003). Act once, ' +\n 'then assert with a web-first assertion; a loop that only re-reads state is fine. The actions are ' +\n \"Playwright's input and navigation calls plus the app's declared actionHelpers. A loop that must act — \" +\n 'polling a background result by reloading — is a reviewed entry whose anchor names that one loop, with ' +\n 'its why; an entry that no longer matches a loop is stale.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run: (context) =>\n anchoredFindings(\n context\n .files(E2E_SCOPE)\n .map((file) => ({ file, hits: retriedActions(file, facts.actionHelpers) })),\n facts.reviewed,\n 'its anchor names no retry loop that acts',\n ),\n },\n ];\n },\n};\n\nexport const swallowedActionGuard: GenericGuardDefinition<E2eActionFacts> = {\n id: 'swallowedE2eActions',\n absentWhen: 'the repository has no end-to-end suite',\n absentOnlyWithoutRoot: 'e2e',\n factProblems: e2eActionProblems,\n build(facts) {\n return [\n {\n title: 'no end-to-end action or state read swallows its failure',\n description:\n 'A .catch() or a try/catch that discards a failed click, fill, or navigation lets the next step run on a ' +\n 'page the action never reached, and one on isVisible(), isEnabled(), or isChecked() turns an ambiguous ' +\n '(strict-mode) locator into a silent \"no\". Let the failure throw: an action whose outcome is uncertain ' +\n \"goes through the suite's outcome-verified helper, and a branch reads state with no catch. A catch that \" +\n 'must stay is a reviewed entry whose anchor names that one occurrence, with its why; an entry that no ' +\n 'longer matches one is stale.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run: (context) =>\n anchoredFindings(\n context\n .files(E2E_SCOPE)\n .map((file) => ({ file, hits: swallowedActions(file, facts.actionHelpers) })),\n facts.reviewed,\n 'its anchor names no swallowed action or state read',\n ),\n },\n ];\n },\n};\n"]}
1
+ {"version":3,"file":"e2e-actions.js","sourceRoot":"","sources":["../../../src/modules/repo/e2e-actions.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Scope } from '../../node/reader.js';\nimport { viewOfText, type FileView } from '../../reader/view.js';\nimport { calleeName, forEachNode } from '../../support/ast.js';\nimport { resolveRelative } from '../../support/paths.js';\nimport {\n anchoredFindings,\n anchoredProblems,\n type AnchoredEntry,\n type Occurrence,\n} from '../../support/anchored-review.js';\nimport { namesProblem, objectProblem } from '../../support/facts.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\n// Playwright's locator and page operations that act on the page: input, and navigation.\nconst PLAYWRIGHT_ACTIONS: ReadonlySet<string> = new Set([\n 'check',\n 'clear',\n 'click',\n 'dblclick',\n 'dispatchEvent',\n 'dragTo',\n 'fill',\n 'goBack',\n 'goForward',\n 'goto',\n 'hover',\n 'press',\n 'pressSequentially',\n 'reload',\n 'selectOption',\n 'selectText',\n 'setChecked',\n 'setInputFiles',\n 'tap',\n 'type',\n 'uncheck',\n]);\n\n// Playwright's state reads: strict locators throw when a locator matches more than one element.\nconst STATE_PROBES: ReadonlySet<string> = new Set([\n 'isChecked',\n 'isDisabled',\n 'isEditable',\n 'isEnabled',\n 'isHidden',\n 'isVisible',\n]);\n\nconst E2E_SCOPE: Scope = {\n roots: ['e2e', 'hostedE2e'],\n kinds: ['code'],\n tests: true,\n optionalRoots: ['hostedE2e'],\n};\n\nconst actionNamesOf = (helpers: readonly string[]) => new Set([...PLAYWRIGHT_ACTIONS, ...helpers]);\n\nconst isFunctionNode = (node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression =>\n ts.isArrowFunction(node) || ts.isFunctionExpression(node);\n\n// `expect(fn).toPass(…)` and `expect.poll(fn)` retry `fn` until it passes.\nfunction retriedCallbacks(sf: ts.SourceFile): Array<ts.ArrowFunction | ts.FunctionExpression> {\n const callbacks: Array<ts.ArrowFunction | ts.FunctionExpression> = [];\n forEachNode(sf, (node) => {\n if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return;\n const method = node.expression.name.text;\n const target = node.expression.expression;\n if (method === 'toPass' && ts.isCallExpression(target)) {\n const fn = target.arguments[0];\n if (fn && isFunctionNode(fn) && calleeName(target)?.toLowerCase().endsWith('expect'))\n callbacks.push(fn);\n }\n if (method === 'poll' && calleeName(node) === 'poll') {\n const fn = node.arguments[0];\n if (fn && isFunctionNode(fn)) callbacks.push(fn);\n }\n });\n return callbacks;\n}\n\nconst isRepeatingLoop = (\n node: ts.Node,\n): node is ts.ForStatement | ts.WhileStatement | ts.DoStatement =>\n ts.isForStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node);\n\ntype FunctionBodies = ReadonlyMap<string, readonly ts.Node[]>;\n\nfunction functionBodiesOf(sf: ts.SourceFile): FunctionBodies {\n const bodies = new Map<string, ts.Node[]>();\n const add = (name: string, body: ts.Node) =>\n bodies.set(name, [...(bodies.get(name) ?? []), body]);\n forEachNode(sf, (node) => {\n if (ts.isFunctionDeclaration(node) && node.name && node.body) add(node.name.text, node.body);\n if (\n ts.isVariableDeclaration(node) &&\n ts.isIdentifier(node.name) &&\n node.initializer &&\n isFunctionNode(node.initializer)\n )\n add(node.name.text, node.initializer.body);\n });\n return bodies;\n}\n\nconst SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];\n\nfunction moduleCandidates(base: string): string[] {\n const stem = base.replace(/\\.(?:[cm]?js|jsx)$/, '');\n return [\n base,\n ...SOURCE_EXTENSIONS.map((ext) => stem + ext),\n ...SOURCE_EXTENSIONS.map((ext) => `${base}/index${ext}`),\n ];\n}\n\n// Where an imported function lives: the file and the name it is declared under there.\nfunction namedImportsOf(\n file: FileView,\n paths: ReadonlySet<string>,\n): Map<string, { path: string; name: string }> {\n const imports = new Map<string, { path: string; name: string }>();\n for (const statement of file.sourceFile.statements) {\n if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))\n continue;\n const bindings = statement.importClause?.namedBindings;\n if (!bindings || !ts.isNamedImports(bindings)) continue;\n const base = resolveRelative(file.path, statement.moduleSpecifier.text)?.path;\n const target = base && moduleCandidates(base).find((candidate) => paths.has(candidate));\n if (!target) continue;\n for (const element of bindings.elements)\n imports.set(element.name.text, {\n path: target,\n name: (element.propertyName ?? element.name).text,\n });\n }\n return imports;\n}\n\n// Finds the first action a piece of code performs, directly or through the plain function calls it\n// makes into functions declared in the same file or imported from the suite's own files.\nexport interface ActionReach {\n actionIn(file: FileView, root: ts.Node): string[] | undefined;\n}\n\nexport function createActionReach(\n files: readonly FileView[],\n helpers: readonly string[] = [],\n): ActionReach {\n const actions = actionNamesOf(helpers);\n const byPath = new Map(files.map((file) => [file.path, file]));\n const paths = new Set(byPath.keys());\n const bodiesCache = new Map<string, FunctionBodies>();\n const importsCache = new Map<string, Map<string, { path: string; name: string }>>();\n const bodiesOf = (file: FileView) => {\n let bodies = bodiesCache.get(file.path);\n if (!bodies) bodiesCache.set(file.path, (bodies = functionBodiesOf(file.sourceFile)));\n return bodies;\n };\n const importsOf = (file: FileView) => {\n let imports = importsCache.get(file.path);\n if (!imports) importsCache.set(file.path, (imports = namedImportsOf(file, paths)));\n return imports;\n };\n\n function search(file: FileView, root: ts.Node, seen: Set<string>): string[] | undefined {\n let found: string[] | undefined;\n forEachNode(root, (node) => {\n if (found || !ts.isCallExpression(node)) return;\n const name = calleeName(node);\n if (!name) return;\n if (actions.has(name)) {\n found = [name];\n return;\n }\n if (!ts.isIdentifier(node.expression)) return;\n const imported = importsOf(file).get(name);\n const local = bodiesOf(file).get(name);\n const owner = local ? file : imported ? byPath.get(imported.path) : undefined;\n const declared = local ? name : imported?.name;\n if (!owner || !declared) return;\n const key = `${owner.path}#${declared}`;\n if (seen.has(key)) return;\n seen.add(key);\n for (const body of bodiesOf(owner).get(declared) ?? []) {\n const inner = search(owner, body, seen);\n if (inner) {\n found = [name, ...inner];\n return;\n }\n }\n });\n return found;\n }\n\n return { actionIn: (file, root) => search(file, root, new Set()) };\n}\n\n// `['openWork', 'goto']` reads \"goto() through openWork\".\nfunction describeAction(chain: readonly string[]): string {\n const action = `${chain[chain.length - 1]}()`;\n return chain.length > 1 ? `${action} through ${chain.slice(0, -1).join(' → ')}` : action;\n}\n\nconst within = (inner: ts.Node, outer: ts.Node): boolean =>\n inner !== outer && inner.pos >= outer.pos && inner.end <= outer.end;\n\nconst viewOf = (source: FileView | string) =>\n typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;\n\nexport function retriedActions(\n source: FileView | string,\n helpers: readonly string[] = [],\n reach?: ActionReach,\n): Occurrence[] {\n const file = viewOf(source);\n const sf = file.sourceFile;\n const reachFrom = reach ?? createActionReach([file], helpers);\n const loops: ts.Node[] = [];\n forEachNode(sf, (node) => {\n if (isRepeatingLoop(node)) loops.push(node);\n });\n const acting = [\n ...retriedCallbacks(sf).map((node) => ({\n node,\n body: node.body,\n kind: 'a retry loop',\n tail: ' until it passes',\n })),\n ...loops.map((node) => ({ node, body: node, kind: 'a for / while / do loop', tail: '' })),\n ].flatMap((loop) => {\n const chain = reachFrom.actionIn(file, loop.body);\n return chain ? [{ ...loop, chain }] : [];\n });\n return acting\n .filter((loop) => !acting.some((outer) => within(loop.node, outer.node)))\n .sort((a, b) => a.node.getStart(sf) - b.node.getStart(sf))\n .map((loop) => ({\n start: loop.node.getStart(sf),\n end: loop.node.getEnd(),\n detail: `${loop.kind} repeats ${describeAction(loop.chain)}${loop.tail}`,\n }));\n}\n\nfunction catchesRejection(handler: ts.Expression): boolean {\n if (!isFunctionNode(handler)) return true;\n let rethrows = false;\n forEachNode(handler.body, (node) => {\n if (ts.isThrowStatement(node)) rethrows = true;\n });\n return !rethrows;\n}\n\n// The call a `.catch()` is chained onto: `x.click().catch(…)` → the `click()` call.\nfunction caughtCall(catchCall: ts.CallExpression): ts.CallExpression | undefined {\n if (!ts.isPropertyAccessExpression(catchCall.expression)) return undefined;\n const receiver = catchCall.expression.expression;\n const call = ts.isParenthesizedExpression(receiver) ? receiver.expression : receiver;\n return ts.isCallExpression(call) ? call : undefined;\n}\n\nexport function swallowedActions(\n source: FileView | string,\n helpers: readonly string[] = [],\n reach?: ActionReach,\n): Occurrence[] {\n const file = viewOf(source);\n const sf = file.sourceFile;\n const reachFrom = reach ?? createActionReach([file], helpers);\n const hits: Occurrence[] = [];\n forEachNode(sf, (node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'catch' &&\n node.arguments.length === 1 &&\n catchesRejection(node.arguments[0])\n ) {\n const call = caughtCall(node);\n if (!call) return;\n const name = calleeName(call);\n const chain = reachFrom.actionIn(file, call);\n if (chain)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a .catch() swallows a failed ${describeAction(chain)}`,\n });\n else if (name && STATE_PROBES.has(name))\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a .catch() turns a failed ${name}() — an ambiguous locator included — into a value`,\n });\n }\n if (ts.isTryStatement(node) && node.catchClause) {\n let rethrows = false;\n forEachNode(node.catchClause.block, (inner) => {\n if (ts.isThrowStatement(inner)) rethrows = true;\n });\n if (rethrows) return;\n const chain = reachFrom.actionIn(file, node.tryBlock);\n if (chain)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a try/catch swallows a failed ${describeAction(chain)}`,\n });\n }\n });\n return hits;\n}\n\nfunction withReach(\n files: readonly FileView[],\n helpers: readonly string[],\n find: (file: FileView, reach: ActionReach) => Occurrence[],\n): Array<{ file: FileView; hits: Occurrence[] }> {\n const reach = createActionReach(files, helpers);\n return files.map((file) => ({ file, hits: find(file, reach) }));\n}\n\nexport interface E2eActionFacts {\n actionHelpers: readonly string[];\n reviewed: readonly AnchoredEntry[];\n}\n\nfunction e2eActionProblems(facts: E2eActionFacts): string[] {\n const problems: string[] = [];\n if (!objectProblem(facts, 'the facts', problems)) return problems;\n namesProblem(facts.actionHelpers, 'actionHelpers', problems, { nonEmpty: false });\n anchoredProblems(facts.reviewed, 'reviewed', problems);\n return problems;\n}\n\nexport const retryLoopActionGuard: GenericGuardDefinition<E2eActionFacts> = {\n id: 'retryLoopActions',\n absentWhen: 'the repository has no end-to-end suite',\n absentOnlyWithoutRoot: 'e2e',\n factProblems: e2eActionProblems,\n build(facts) {\n return [\n {\n title: 'no end-to-end retry loop repeats a user action',\n description:\n 'A toPass() or expect.poll() callback, or a for (;;) / while / do loop, that clicks, types, or navigates ' +\n 'repeats the action until it happens to work, so the failure a real user would hit passes as green ' +\n '(RULE-STD-QUALITY-003). Walking a list is written for…of, which is not a repeat. Act once, ' +\n 'then assert with a web-first assertion; a loop that only re-reads state is fine. The actions are ' +\n \"Playwright's input and navigation calls plus the app's declared actionHelpers, reached directly or through \" +\n 'a plain call to a function the suite declares in the same file or imports from its own files. A loop that must act — ' +\n 'polling a background result by reloading — is a reviewed entry whose anchor names that one loop, with ' +\n 'its why; an entry that no longer matches a loop is stale.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run: (context) =>\n anchoredFindings(\n withReach(context.files(E2E_SCOPE), facts.actionHelpers, (file, reach) =>\n retriedActions(file, facts.actionHelpers, reach),\n ),\n facts.reviewed,\n 'its anchor names no retry loop that acts',\n ),\n },\n ];\n },\n};\n\nexport const swallowedActionGuard: GenericGuardDefinition<E2eActionFacts> = {\n id: 'swallowedE2eActions',\n absentWhen: 'the repository has no end-to-end suite',\n absentOnlyWithoutRoot: 'e2e',\n factProblems: e2eActionProblems,\n build(facts) {\n return [\n {\n title: 'no end-to-end action or state read swallows its failure',\n description:\n 'A .catch() or a try/catch that discards a failed click, fill, or navigation — made directly or through a plain ' +\n \"call to one of the suite's own functions — lets the next step run on a \" +\n 'page the action never reached, and one on isVisible(), isEnabled(), or isChecked() turns an ambiguous ' +\n '(strict-mode) locator into a silent \"no\". Let the failure throw: an action whose outcome is uncertain ' +\n \"goes through the suite's outcome-verified helper, and a branch reads state with no catch. A catch that \" +\n 'must stay is a reviewed entry whose anchor names that one occurrence, with its why; an entry that no ' +\n 'longer matches one is stale.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run: (context) =>\n anchoredFindings(\n withReach(context.files(E2E_SCOPE), facts.actionHelpers, (file, reach) =>\n swallowedActions(file, facts.actionHelpers, reach),\n ),\n facts.reviewed,\n 'its anchor names no swallowed action or state read',\n ),\n },\n ];\n },\n};\n"]}
@@ -2,6 +2,7 @@ export interface VitestAssertion {
2
2
  status?: string;
3
3
  title?: string;
4
4
  fullName?: string;
5
+ duration?: number | null;
5
6
  failureMessages?: string[];
6
7
  }
7
8
  export interface VitestFileResult {
@@ -1 +1 @@
1
- {"version":3,"file":"vitest-report.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/vitest-report.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAGD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAcvE;AAID,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,GAAG;IACtE,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;CAC7B,CAgCA;AAED,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,MAAM,EACN,aAAa,GACd,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,YAAY,EAAE,CAAC;CAC/B,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAWhD;AAED,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,YAAY,EAAE,GAAG,MAAM,CAUzE"}
1
+ {"version":3,"file":"vitest-report.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/vitest-report.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAGD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAcvE;AAcD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,GAAG;IACtE,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;CAC7B,CAoCA;AAED,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,MAAM,EACN,aAAa,GACd,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,YAAY,EAAE,CAAC;CAC/B,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAWhD;AAED,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,YAAY,EAAE,GAAG,MAAM,CAUzE"}
@@ -17,6 +17,15 @@ export function parseVitestJson(stdout) {
17
17
  }
18
18
  }
19
19
  }
20
+ const TIMEOUT_PLACEHOLDER = 'Error: STACK_TRACE_ERROR';
21
+ // Vitest reports a test that passed its time limit with the stack of a placeholder error, so the
22
+ // report carries `STACK_TRACE_ERROR` where the "timed out" message should be.
23
+ function readableFailureMessage(message, durationMs) {
24
+ if (!message.startsWith(TIMEOUT_PLACEHOLDER))
25
+ return message;
26
+ const ran = typeof durationMs === 'number' ? ` after ${Math.round(durationMs)} ms` : '';
27
+ return `Timed out${ran}: the test passed its time limit.${message.slice(TIMEOUT_PLACEHOLDER.length)}`;
28
+ }
20
29
  // A test file that failed to load carries its error as the file's own `message` and adds nothing
21
30
  // to `numFailedTests`.
22
31
  export function collectVitestFailures(report) {
@@ -31,7 +40,11 @@ export function collectVitestFailures(report) {
31
40
  const title = a.fullName || a.title || '';
32
41
  if (a.status === 'failed') {
33
42
  failedTests += 1;
34
- failures.push({ file: file.name, title, messages: a.failureMessages ?? [] });
43
+ failures.push({
44
+ file: file.name,
45
+ title,
46
+ messages: (a.failureMessages ?? []).map((m) => readableFailureMessage(m, a.duration)),
47
+ });
35
48
  }
36
49
  else if (a.status === 'skipped' || a.status === 'pending' || a.status === 'todo') {
37
50
  skippedTests.push({ file: file.name, title, status: a.status });
@@ -1 +1 @@
1
- {"version":3,"file":"vitest-report.js","sourceRoot":"","sources":["../../../src/node/tooling/vitest-report.ts"],"names":[],"mappings":"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","sourcesContent":["export interface VitestAssertion {\n status?: string;\n title?: string;\n fullName?: string;\n failureMessages?: string[];\n}\n\nexport interface VitestFileResult {\n name: string;\n status?: string;\n message?: string;\n assertionResults?: VitestAssertion[];\n}\n\nexport interface VitestJsonReport {\n testResults?: VitestFileResult[];\n numPassedTests?: number;\n numFailedTests?: number;\n numPendingTests?: number;\n numTodoTests?: number;\n numTotalTests?: number;\n}\n\nexport interface TestFailure {\n file: string;\n title: string;\n messages: string[];\n}\n\nexport interface SuiteFailure {\n file: string;\n message: string;\n}\n\nexport interface SkippedTest {\n file: string;\n title: string;\n status: string;\n}\n\nexport interface VitestTotals {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n failedSuites: number;\n}\n\n// Vitest may print other text around the JSON object.\nexport function parseVitestJson(stdout: string): VitestJsonReport | null {\n const trimmed = stdout.trim();\n try {\n return JSON.parse(trimmed) as VitestJsonReport;\n } catch {\n const first = trimmed.indexOf('{');\n const last = trimmed.lastIndexOf('}');\n if (first === -1 || last === -1 || last < first) return null;\n try {\n return JSON.parse(trimmed.slice(first, last + 1)) as VitestJsonReport;\n } catch {\n return null;\n }\n }\n}\n\n// A test file that failed to load carries its error as the file's own `message` and adds nothing\n// to `numFailedTests`.\nexport function collectVitestFailures(report: VitestJsonReport | null): {\n failures: TestFailure[];\n suiteFailures: SuiteFailure[];\n skippedTests: SkippedTest[];\n totals: VitestTotals | null;\n} {\n if (!report) return { failures: [], suiteFailures: [], skippedTests: [], totals: null };\n const failures: TestFailure[] = [];\n const suiteFailures: SuiteFailure[] = [];\n const skippedTests: SkippedTest[] = [];\n for (const file of report.testResults ?? []) {\n let failedTests = 0;\n for (const a of file.assertionResults ?? []) {\n const title = a.fullName || a.title || '';\n if (a.status === 'failed') {\n failedTests += 1;\n failures.push({ file: file.name, title, messages: a.failureMessages ?? [] });\n } else if (a.status === 'skipped' || a.status === 'pending' || a.status === 'todo') {\n skippedTests.push({ file: file.name, title, status: a.status });\n }\n }\n if (file.status === 'failed' && failedTests === 0) {\n suiteFailures.push({\n file: file.name,\n message: file.message || '(no error message in the report)',\n });\n }\n }\n const totals: VitestTotals = {\n passed: report.numPassedTests ?? 0,\n failed: report.numFailedTests ?? 0,\n // `.skip` and `xit` land in numPendingTests; `.todo` is counted on its own.\n skipped: (report.numPendingTests ?? 0) + (report.numTodoTests ?? 0),\n total: report.numTotalTests ?? 0,\n failedSuites: suiteFailures.length,\n };\n return { failures, suiteFailures, skippedTests, totals };\n}\n\nexport function vitestStageVerdict({\n code,\n totals,\n suiteFailures,\n}: {\n code: number;\n totals: VitestTotals | null;\n suiteFailures: SuiteFailure[];\n}): { ok: boolean; skippedReason: string | null } {\n const skipped = totals?.skipped ?? 0;\n const ok =\n code === 0 &&\n totals !== null &&\n totals.failed === 0 &&\n skipped === 0 &&\n suiteFailures.length === 0;\n const skippedReason =\n skipped > 0 ? `${skipped} skipped/pending test(s) make this stage non-green.` : null;\n return { ok, skippedReason };\n}\n\nexport function formatSuiteFailures(suiteFailures: SuiteFailure[]): string {\n return suiteFailures\n .map(\n (suite) =>\n `Failed to load: ${suite.file}\\n${suite.message\n .split('\\n')\n .map((line) => ` ${line}`)\n .join('\\n')}`,\n )\n .join('\\n\\n');\n}\n"]}
1
+ {"version":3,"file":"vitest-report.js","sourceRoot":"","sources":["../../../src/node/tooling/vitest-report.ts"],"names":[],"mappings":"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","sourcesContent":["export interface VitestAssertion {\n status?: string;\n title?: string;\n fullName?: string;\n duration?: number | null;\n failureMessages?: string[];\n}\n\nexport interface VitestFileResult {\n name: string;\n status?: string;\n message?: string;\n assertionResults?: VitestAssertion[];\n}\n\nexport interface VitestJsonReport {\n testResults?: VitestFileResult[];\n numPassedTests?: number;\n numFailedTests?: number;\n numPendingTests?: number;\n numTodoTests?: number;\n numTotalTests?: number;\n}\n\nexport interface TestFailure {\n file: string;\n title: string;\n messages: string[];\n}\n\nexport interface SuiteFailure {\n file: string;\n message: string;\n}\n\nexport interface SkippedTest {\n file: string;\n title: string;\n status: string;\n}\n\nexport interface VitestTotals {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n failedSuites: number;\n}\n\n// Vitest may print other text around the JSON object.\nexport function parseVitestJson(stdout: string): VitestJsonReport | null {\n const trimmed = stdout.trim();\n try {\n return JSON.parse(trimmed) as VitestJsonReport;\n } catch {\n const first = trimmed.indexOf('{');\n const last = trimmed.lastIndexOf('}');\n if (first === -1 || last === -1 || last < first) return null;\n try {\n return JSON.parse(trimmed.slice(first, last + 1)) as VitestJsonReport;\n } catch {\n return null;\n }\n }\n}\n\nconst TIMEOUT_PLACEHOLDER = 'Error: STACK_TRACE_ERROR';\n\n// Vitest reports a test that passed its time limit with the stack of a placeholder error, so the\n// report carries `STACK_TRACE_ERROR` where the \"timed out\" message should be.\nfunction readableFailureMessage(message: string, durationMs?: number | null): string {\n if (!message.startsWith(TIMEOUT_PLACEHOLDER)) return message;\n const ran = typeof durationMs === 'number' ? ` after ${Math.round(durationMs)} ms` : '';\n return `Timed out${ran}: the test passed its time limit.${message.slice(TIMEOUT_PLACEHOLDER.length)}`;\n}\n\n// A test file that failed to load carries its error as the file's own `message` and adds nothing\n// to `numFailedTests`.\nexport function collectVitestFailures(report: VitestJsonReport | null): {\n failures: TestFailure[];\n suiteFailures: SuiteFailure[];\n skippedTests: SkippedTest[];\n totals: VitestTotals | null;\n} {\n if (!report) return { failures: [], suiteFailures: [], skippedTests: [], totals: null };\n const failures: TestFailure[] = [];\n const suiteFailures: SuiteFailure[] = [];\n const skippedTests: SkippedTest[] = [];\n for (const file of report.testResults ?? []) {\n let failedTests = 0;\n for (const a of file.assertionResults ?? []) {\n const title = a.fullName || a.title || '';\n if (a.status === 'failed') {\n failedTests += 1;\n failures.push({\n file: file.name,\n title,\n messages: (a.failureMessages ?? []).map((m) => readableFailureMessage(m, a.duration)),\n });\n } else if (a.status === 'skipped' || a.status === 'pending' || a.status === 'todo') {\n skippedTests.push({ file: file.name, title, status: a.status });\n }\n }\n if (file.status === 'failed' && failedTests === 0) {\n suiteFailures.push({\n file: file.name,\n message: file.message || '(no error message in the report)',\n });\n }\n }\n const totals: VitestTotals = {\n passed: report.numPassedTests ?? 0,\n failed: report.numFailedTests ?? 0,\n // `.skip` and `xit` land in numPendingTests; `.todo` is counted on its own.\n skipped: (report.numPendingTests ?? 0) + (report.numTodoTests ?? 0),\n total: report.numTotalTests ?? 0,\n failedSuites: suiteFailures.length,\n };\n return { failures, suiteFailures, skippedTests, totals };\n}\n\nexport function vitestStageVerdict({\n code,\n totals,\n suiteFailures,\n}: {\n code: number;\n totals: VitestTotals | null;\n suiteFailures: SuiteFailure[];\n}): { ok: boolean; skippedReason: string | null } {\n const skipped = totals?.skipped ?? 0;\n const ok =\n code === 0 &&\n totals !== null &&\n totals.failed === 0 &&\n skipped === 0 &&\n suiteFailures.length === 0;\n const skippedReason =\n skipped > 0 ? `${skipped} skipped/pending test(s) make this stage non-green.` : null;\n return { ok, skippedReason };\n}\n\nexport function formatSuiteFailures(suiteFailures: SuiteFailure[]): string {\n return suiteFailures\n .map(\n (suite) =>\n `Failed to load: ${suite.file}\\n${suite.message\n .split('\\n')\n .map((line) => ` ${line}`)\n .join('\\n')}`,\n )\n .join('\\n\\n');\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/repo-guards",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",