code-foundry 0.34.3 → 0.34.4
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/CHANGELOG.md +7 -0
- package/package.json +4 -3
- package/src/lib/test-discovery.mjs +39 -0
- package/src/runtime.mjs +61 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.34.4](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.3...v0.34.4) (2026-08-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* split native test suites by category ([93c9ede](https://github.com/0xPlayerOne/code-foundry/commit/93c9ede6e202a790befe1ee0e57633e3d01dc428))
|
|
9
|
+
|
|
3
10
|
## [0.34.3](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.2...v0.34.3) (2026-08-02)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.4",
|
|
4
4
|
"description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"format:check": "node --check src/cli.mjs",
|
|
45
45
|
"lint": "node --check src/cli.mjs",
|
|
46
46
|
"test": "node --test",
|
|
47
|
+
"test:unit": "node --test",
|
|
47
48
|
"type-check": "tsc --project jsconfig.json",
|
|
48
49
|
"pack:check": "npm pack --dry-run",
|
|
49
50
|
"prepublishOnly": "npm test && npm run pack:check"
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"access": "public"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"@types/node": "^
|
|
56
|
-
"typescript": "^
|
|
56
|
+
"@types/node": "^26.0.0",
|
|
57
|
+
"typescript": "^7.0.2"
|
|
57
58
|
}
|
|
58
59
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const TEST_FILE_PATTERN = /(?:\.(?:test|spec|unit|integration|smoke|e2e)\.(?:[cm]?[jt]sx?|py)|(?:^|\/)test_[^/]+\.py$|(?:^|\/)tests\/.*\.rs$)/i
|
|
4
|
+
|
|
5
|
+
/** @param {string} file */
|
|
6
|
+
export function isTestFile(file) {
|
|
7
|
+
return TEST_FILE_PATTERN.test(file.replaceAll('\\', '/'))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** @param {string} file */
|
|
11
|
+
export function testCategory(file) {
|
|
12
|
+
const normalized = file.replaceAll('\\', '/').toLowerCase()
|
|
13
|
+
const base = normalized.split('/').at(-1) ?? normalized
|
|
14
|
+
|
|
15
|
+
if (/(^|\/)(e2e|end-to-end|acceptance)(\/|$)|(?:^|[._-])(e2e|acceptance)(?:[._-]|$)/.test(normalized)) {
|
|
16
|
+
return 'e2e'
|
|
17
|
+
}
|
|
18
|
+
if (/(^|\/)(integration)(\/|$)|(?:^|[._-])integration(?:[._-]|$)/.test(normalized)) {
|
|
19
|
+
return 'integration'
|
|
20
|
+
}
|
|
21
|
+
if (/(^|\/)(smoke|live)(\/|$)|(?:^|[._-])(smoke|live)(?:[._-]|$)/.test(normalized)) {
|
|
22
|
+
return 'smoke'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (normalized.endsWith('.rs') && /(^|\/)tests\//.test(normalized)) {
|
|
26
|
+
return 'integration'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Rust files under tests/ compile as integration-test binaries. An explicit
|
|
30
|
+
// suffix still lets smoke and integration names win when they are present.
|
|
31
|
+
if (base.endsWith('_smoke.rs')) return 'smoke'
|
|
32
|
+
if (base.endsWith('_integration.rs')) return 'integration'
|
|
33
|
+
return 'unit'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {string[]} files @param {'unit'|'integration'|'e2e'|'smoke'} task */
|
|
37
|
+
export function classifyTestFiles(files, task) {
|
|
38
|
+
return files.filter((file) => isTestFile(file) && testCategory(file) === task)
|
|
39
|
+
}
|
package/src/runtime.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { resolve } from 'node:path'
|
|
|
6
6
|
import { spawnSync } from 'node:child_process'
|
|
7
7
|
import { detectPackageManager, resolveProfile } from './lib/profile.mjs'
|
|
8
8
|
import { configured, readConfig } from './lib/config.mjs'
|
|
9
|
+
import { classifyTestFiles } from './lib/test-discovery.mjs'
|
|
9
10
|
import { classifyValidationMode, evaluateValidationGate } from './lib/validation-policy.mjs'
|
|
10
11
|
import { readReleaseConfig, validateGeneratedReleaseDiff } from './lib/release-policy.mjs'
|
|
11
12
|
|
|
@@ -126,6 +127,19 @@ function capture(command, args = []) {
|
|
|
126
127
|
return result.status === 0 ? result.stdout.trim() : ''
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/** @returns {string[]} */
|
|
131
|
+
function repositoryTestFiles() {
|
|
132
|
+
return capture('git', ['ls-files'])
|
|
133
|
+
.split(/\r?\n/)
|
|
134
|
+
.map((file) => file.trim())
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @param {'unit'|'integration'|'e2e'|'smoke'} task */
|
|
139
|
+
function taskTestFiles(task) {
|
|
140
|
+
return classifyTestFiles(repositoryTestFiles(), task)
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
/** @param {string[]} args @returns {[string|null, string[]]} */
|
|
130
144
|
function packageCommand(args) {
|
|
131
145
|
switch (packageManager) {
|
|
@@ -212,8 +226,16 @@ function relevant(task) {
|
|
|
212
226
|
return (js && hasRootJavascriptProject()) || (python && hasRootPythonProject()) || (rust && hasRootRustProject())
|
|
213
227
|
}
|
|
214
228
|
if (['unit', 'integration', 'e2e', 'smoke'].includes(task)) {
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
if (taskTestFiles(/** @type {'unit'|'integration'|'e2e'|'smoke'} */ (task)).length > 0) return true
|
|
230
|
+
// A project with inline Rust/Python/JavaScript unit tests is still a unit
|
|
231
|
+
// test target even when it has no conventional test file path. Other
|
|
232
|
+
// categories must have an explicit script or discoverable test files so
|
|
233
|
+
// their workflow jobs become skipped instead of successful no-ops.
|
|
234
|
+
if (task === 'unit') {
|
|
235
|
+
return (js && hasRootJavascriptProject()) || (python && hasRootPythonProject()) ||
|
|
236
|
+
(rust && hasRootRustProject()) || (hasLanguage('solidity') && hasRootJavascriptProject())
|
|
237
|
+
}
|
|
238
|
+
return false
|
|
217
239
|
}
|
|
218
240
|
return true
|
|
219
241
|
}
|
|
@@ -274,17 +296,49 @@ function ci(task) {
|
|
|
274
296
|
smoke: ['test:smoke', 'smoke'],
|
|
275
297
|
}
|
|
276
298
|
const scripts = scriptsByTask[task]
|
|
277
|
-
|
|
278
|
-
|
|
299
|
+
const scripted = scripts ? runScript(scripts) : false
|
|
300
|
+
const testFiles = taskTestFiles(/** @type {'unit'|'integration'|'e2e'|'smoke'} */ (task))
|
|
301
|
+
const javascriptTests = testFiles.filter((file) => /\.(?:[cm]?[jt]sx?)$/i.test(file))
|
|
302
|
+
const pythonTests = testFiles.filter((file) => /\.py$/i.test(file))
|
|
303
|
+
const rustTests = testFiles.filter((file) => /\.rs$/i.test(file))
|
|
304
|
+
|
|
305
|
+
// A repository script is authoritative for that package. When it is absent,
|
|
306
|
+
// use Bun's native runner with only the discovered files for the requested
|
|
307
|
+
// category; this prevents smoke/integration files from being re-run as unit
|
|
308
|
+
// tests and avoids no-op category jobs.
|
|
309
|
+
if (!scripted && (hasLanguage('typescript') || hasLanguage('javascript') || hasLanguage('solidity')) && hasRootJavascriptProject()) {
|
|
310
|
+
if (javascriptTests.length > 0 || task === 'unit') run('bun', ['test', ...javascriptTests])
|
|
311
|
+
}
|
|
312
|
+
|
|
279
313
|
if (hasLanguage('python') && hasRootPythonProject()) {
|
|
280
314
|
const python = existsSync(resolve(root, '.venv/bin/python')) ? resolve(root, '.venv/bin/python') : 'python'
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
315
|
+
if (pythonTests.length > 0 || task === 'unit') run(python, ['-m', 'pytest', ...pythonTests])
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (hasLanguage('rust') && hasRootRustProject()) {
|
|
319
|
+
if (task === 'unit') {
|
|
320
|
+
if (existsSync(resolve(root, 'src/lib.rs'))) run('cargo', ['test', '--lib'])
|
|
321
|
+
if (existsSync(resolve(root, 'src/main.rs'))) run('cargo', ['test', '--bin', packageName()])
|
|
322
|
+
if (!existsSync(resolve(root, 'src/lib.rs')) && !existsSync(resolve(root, 'src/main.rs'))) run('cargo', ['test'])
|
|
323
|
+
} else {
|
|
324
|
+
for (const file of rustTests) {
|
|
325
|
+
const match = file.match(/^tests\/(.+)\.rs$/)
|
|
326
|
+
if (match && !match[1].includes('/')) run('cargo', ['test', '--test', match[1]])
|
|
327
|
+
}
|
|
284
328
|
}
|
|
285
329
|
}
|
|
286
330
|
}
|
|
287
331
|
|
|
332
|
+
function packageName() {
|
|
333
|
+
const cargo = readFileSafe(resolve(root, 'Cargo.toml'))
|
|
334
|
+
return cargo?.match(/^name\s*=\s*"([^"]+)"/m)?.[1] ?? 'app'
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** @param {string} file */
|
|
338
|
+
function readFileSafe(file) {
|
|
339
|
+
try { return readFileSync(file, 'utf8') } catch { return null }
|
|
340
|
+
}
|
|
341
|
+
|
|
288
342
|
/** @param {string} task @param {string} ecosystem */
|
|
289
343
|
function security(task, ecosystem) {
|
|
290
344
|
if (task === 'profile') {
|