@telorun/test 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/suite.js +42 -22
  2. package/package.json +3 -3
  3. package/src/suite.ts +43 -24
package/dist/suite.js CHANGED
@@ -48,17 +48,37 @@ function discoverTests(baseDir, include, exclude, filter) {
48
48
  const includeRe = include.map(globToRegex);
49
49
  const excludeRe = exclude.map(globToRegex);
50
50
  const results = [];
51
+ // Dedupe by realpath: pnpm symlinks workspace packages into multiple
52
+ // node_modules locations, so the same test file can be reached via
53
+ // many paths. Without dedupe, recursive traversal yields the same yaml
54
+ // dozens of times under different prefixes.
55
+ const seen = new Set();
51
56
  for (const entry of entries) {
52
- const normalized = entry.replace(/\\/g, "/");
53
- if (normalized.includes("node_modules/"))
57
+ const rel = entry.replace(/\\/g, "/");
58
+ // Hard-skip node_modules: those are always symlinked workspace dupes
59
+ // (or vendored copies that shouldn't run as workspace tests). The
60
+ // user-facing `exclude` defaults to `__fixtures__` only, but
61
+ // node_modules is a hard architectural skip.
62
+ if (rel.split("/").includes("node_modules"))
54
63
  continue;
55
- if (!includeRe.some((re) => re.test(normalized)))
64
+ if (!includeRe.some((re) => re.test(rel)))
56
65
  continue;
57
- if (excludeRe.some((re) => re.test(normalized)))
66
+ if (excludeRe.some((re) => re.test(rel)))
58
67
  continue;
59
- if (filter && !normalized.includes(filter))
68
+ if (filter && !rel.includes(filter))
60
69
  continue;
61
- results.push(path.resolve(baseDir, entry));
70
+ const abs = path.resolve(baseDir, rel);
71
+ let real;
72
+ try {
73
+ real = fs.realpathSync(abs);
74
+ }
75
+ catch {
76
+ real = abs;
77
+ }
78
+ if (seen.has(real))
79
+ continue;
80
+ seen.add(real);
81
+ results.push(abs);
62
82
  }
63
83
  results.sort();
64
84
  return results;
@@ -66,13 +86,19 @@ function discoverTests(baseDir, include, exclude, filter) {
66
86
  function labelFor(testPath, baseDir) {
67
87
  return path.relative(baseDir, testPath);
68
88
  }
69
- /**
70
- * Parse a .env file into a key-value map.
71
- * Supports KEY=VALUE lines, ignores comments and blank lines.
72
- */
89
+ function tryReadFile(filePath) {
90
+ try {
91
+ return fs.readFileSync(filePath, "utf8");
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
73
97
  function parseEnvFile(content) {
98
+ if (!content)
99
+ return {};
74
100
  const result = {};
75
- for (const line of content.split("\n")) {
101
+ for (const line of content.split(/\r?\n/)) {
76
102
  const trimmed = line.trim();
77
103
  if (!trimmed || trimmed.startsWith("#"))
78
104
  continue;
@@ -81,24 +107,18 @@ function parseEnvFile(content) {
81
107
  continue;
82
108
  const key = trimmed.slice(0, eq).trim();
83
109
  let value = trimmed.slice(eq + 1).trim();
84
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
110
+ if ((value.startsWith('"') && value.endsWith('"')) ||
111
+ (value.startsWith("'") && value.endsWith("'"))) {
85
112
  value = value.slice(1, -1);
86
113
  }
87
114
  result[key] = value;
88
115
  }
89
116
  return result;
90
117
  }
91
- function tryReadFile(filePath) {
92
- try {
93
- return fs.readFileSync(filePath, "utf8");
94
- }
95
- catch {
96
- return "";
97
- }
98
- }
99
118
  /**
100
- * Build an env object for a test manifest by layering .env and .env.local
101
- * from the manifest's directory on top of the current process.env.
119
+ * Loads .env and .env.local files (in that order) from the directory of
120
+ * the manifest, layered under (and overridden by) process.env.
121
+ *
102
122
  * Keys already present in process.env take precedence (same as CLI behaviour).
103
123
  */
104
124
  function buildEnvForManifest(manifestPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/test",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Telo Test module - Test runner for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -33,8 +33,8 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@sinclair/typebox": "^0.34.48",
36
- "@telorun/kernel": "0.4.1",
37
- "@telorun/sdk": "0.3.2"
36
+ "@telorun/kernel": "0.5.0",
37
+ "@telorun/sdk": "0.5.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^20.0.0",
package/src/suite.ts CHANGED
@@ -78,15 +78,33 @@ function discoverTests(
78
78
  const excludeRe = exclude.map(globToRegex);
79
79
 
80
80
  const results: string[] = [];
81
+ // Dedupe by realpath: pnpm symlinks workspace packages into multiple
82
+ // node_modules locations, so the same test file can be reached via
83
+ // many paths. Without dedupe, recursive traversal yields the same yaml
84
+ // dozens of times under different prefixes.
85
+ const seen = new Set<string>();
86
+
81
87
  for (const entry of entries) {
82
- const normalized = entry.replace(/\\/g, "/");
83
- if (normalized.includes("node_modules/")) continue;
84
- if (!includeRe.some((re) => re.test(normalized))) continue;
85
- if (excludeRe.some((re) => re.test(normalized))) continue;
86
- if (filter && !normalized.includes(filter)) continue;
87
- results.push(path.resolve(baseDir, entry));
88
+ const rel = entry.replace(/\\/g, "/");
89
+ // Hard-skip node_modules: those are always symlinked workspace dupes
90
+ // (or vendored copies that shouldn't run as workspace tests). The
91
+ // user-facing `exclude` defaults to `__fixtures__` only, but
92
+ // node_modules is a hard architectural skip.
93
+ if (rel.split("/").includes("node_modules")) continue;
94
+ if (!includeRe.some((re) => re.test(rel))) continue;
95
+ if (excludeRe.some((re) => re.test(rel))) continue;
96
+ if (filter && !rel.includes(filter)) continue;
97
+ const abs = path.resolve(baseDir, rel);
98
+ let real: string;
99
+ try {
100
+ real = fs.realpathSync(abs);
101
+ } catch {
102
+ real = abs;
103
+ }
104
+ if (seen.has(real)) continue;
105
+ seen.add(real);
106
+ results.push(abs);
88
107
  }
89
-
90
108
  results.sort();
91
109
  return results;
92
110
  }
@@ -95,20 +113,28 @@ function labelFor(testPath: string, baseDir: string): string {
95
113
  return path.relative(baseDir, testPath);
96
114
  }
97
115
 
98
- /**
99
- * Parse a .env file into a key-value map.
100
- * Supports KEY=VALUE lines, ignores comments and blank lines.
101
- */
102
- function parseEnvFile(content: string): Record<string, string> {
116
+ function tryReadFile(filePath: string): string | null {
117
+ try {
118
+ return fs.readFileSync(filePath, "utf8");
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ function parseEnvFile(content: string | null): Record<string, string> {
125
+ if (!content) return {};
103
126
  const result: Record<string, string> = {};
104
- for (const line of content.split("\n")) {
127
+ for (const line of content.split(/\r?\n/)) {
105
128
  const trimmed = line.trim();
106
129
  if (!trimmed || trimmed.startsWith("#")) continue;
107
130
  const eq = trimmed.indexOf("=");
108
131
  if (eq === -1) continue;
109
132
  const key = trimmed.slice(0, eq).trim();
110
133
  let value = trimmed.slice(eq + 1).trim();
111
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
134
+ if (
135
+ (value.startsWith('"') && value.endsWith('"')) ||
136
+ (value.startsWith("'") && value.endsWith("'"))
137
+ ) {
112
138
  value = value.slice(1, -1);
113
139
  }
114
140
  result[key] = value;
@@ -116,17 +142,10 @@ function parseEnvFile(content: string): Record<string, string> {
116
142
  return result;
117
143
  }
118
144
 
119
- function tryReadFile(filePath: string): string {
120
- try {
121
- return fs.readFileSync(filePath, "utf8");
122
- } catch {
123
- return "";
124
- }
125
- }
126
-
127
145
  /**
128
- * Build an env object for a test manifest by layering .env and .env.local
129
- * from the manifest's directory on top of the current process.env.
146
+ * Loads .env and .env.local files (in that order) from the directory of
147
+ * the manifest, layered under (and overridden by) process.env.
148
+ *
130
149
  * Keys already present in process.env take precedence (same as CLI behaviour).
131
150
  */
132
151
  function buildEnvForManifest(manifestPath: string): Record<string, string | undefined> {