test: port workspace-level JS tests to a new js-tests workspace package
parent
2f3007ff51
commit
09f8a8268c
|
|
@ -13,7 +13,8 @@
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"ui-tui",
|
"ui-tui",
|
||||||
"ui-tui/packages/*",
|
"ui-tui/packages/*",
|
||||||
"web"
|
"web",
|
||||||
|
"tests-js"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@streamdown/math": "^1.0.2",
|
"@streamdown/math": "^1.0.2",
|
||||||
|
|
@ -19678,6 +19679,15 @@
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"tests-js": {
|
||||||
|
"name": "@hermes/root-tests",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@hermes/root-tests": {
|
||||||
|
"resolved": "tests-js",
|
||||||
|
"link": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"ui-tui",
|
"ui-tui",
|
||||||
"ui-tui/packages/*",
|
"ui-tui/packages/*",
|
||||||
"web"
|
"web",
|
||||||
|
"tests-js"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "echo '✅ Browser tools ready. Run: python run_agent.py --help'",
|
"postinstall": "echo '✅ Browser tools ready. Run: python run_agent.py --help'",
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ import path from 'node:path'
|
||||||
|
|
||||||
import { test } from 'vitest'
|
import { test } from 'vitest'
|
||||||
|
|
||||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..')
|
const REPO_ROOT = path.resolve(__dirname, '..')
|
||||||
const LOCK_PATH = path.join(REPO_ROOT, 'package-lock.json')
|
const LOCK_PATH = path.join(REPO_ROOT, 'package-lock.json')
|
||||||
const TAP = '@assistant-ui/tap'
|
const TAP = '@assistant-ui/tap'
|
||||||
|
|
||||||
|
|
@ -51,18 +51,25 @@ function caretSatisfies(version: string, spec: string): boolean {
|
||||||
function parse(v: string): [number, number, number] {
|
function parse(v: string): [number, number, number] {
|
||||||
const core = v.replace(/^[^0-9]+/, '').split('-')[0].split('+')[0]
|
const core = v.replace(/^[^0-9]+/, '').split('-')[0].split('+')[0]
|
||||||
const parts = core.split('.').slice(0, 3)
|
const parts = core.split('.').slice(0, 3)
|
||||||
while (parts.length < 3) parts.push('0')
|
|
||||||
|
while (parts.length < 3) {parts.push('0')}
|
||||||
|
|
||||||
return [parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10)]
|
return [parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10)]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ver = parse(version)
|
const ver = parse(version)
|
||||||
|
|
||||||
for (const clause of spec.split('||')) {
|
for (const clause of spec.split('||')) {
|
||||||
const trimmed = clause.trim()
|
const trimmed = clause.trim()
|
||||||
if (!trimmed) continue
|
|
||||||
|
if (!trimmed) {continue}
|
||||||
|
|
||||||
if (trimmed.startsWith('^')) {
|
if (trimmed.startsWith('^')) {
|
||||||
const lo = parse(trimmed)
|
const lo = parse(trimmed)
|
||||||
if (ver[0] < lo[0] || (ver[0] === lo[0] && ver[1] < lo[1]) || (ver[0] === lo[0] && ver[1] === lo[1] && ver[2] < lo[2])) continue
|
|
||||||
|
if (ver[0] < lo[0] || (ver[0] === lo[0] && ver[1] < lo[1]) || (ver[0] === lo[0] && ver[1] === lo[1] && ver[2] < lo[2])) {continue}
|
||||||
let hi: [number, number, number]
|
let hi: [number, number, number]
|
||||||
|
|
||||||
if (lo[0] > 0) {
|
if (lo[0] > 0) {
|
||||||
hi = [lo[0] + 1, 0, 0]
|
hi = [lo[0] + 1, 0, 0]
|
||||||
} else if (lo[1] > 0) {
|
} else if (lo[1] > 0) {
|
||||||
|
|
@ -70,6 +77,7 @@ function caretSatisfies(version: string, spec: string): boolean {
|
||||||
} else {
|
} else {
|
||||||
hi = [0, 0, lo[2] + 1]
|
hi = [0, 0, lo[2] + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(ver[0] < hi[0]) ||
|
(ver[0] < hi[0]) ||
|
||||||
(ver[0] === hi[0] && ver[1] < hi[1]) ||
|
(ver[0] === hi[0] && ver[1] < hi[1]) ||
|
||||||
|
|
@ -83,6 +91,7 @@ function caretSatisfies(version: string, spec: string): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,39 +103,52 @@ interface LockPackage {
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockPackages(): Record<string, LockPackage> {
|
function lockPackages(): Record<string, LockPackage> {
|
||||||
|
if (!fs.existsSync(LOCK_PATH)) {return {}}
|
||||||
const lock = JSON.parse(fs.readFileSync(LOCK_PATH, 'utf-8'))
|
const lock = JSON.parse(fs.readFileSync(LOCK_PATH, 'utf-8'))
|
||||||
|
|
||||||
return (lock.packages ?? {}) as Record<string, LockPackage>
|
return (lock.packages ?? {}) as Record<string, LockPackage>
|
||||||
}
|
}
|
||||||
|
|
||||||
function sharedTapVersion(packages: Record<string, LockPackage>): string {
|
function sharedTapVersion(packages: Record<string, LockPackage>): string {
|
||||||
/** The one tap version every install site resolves to. */
|
/** The one tap version every install site resolves to. */
|
||||||
const versions = new Set<string>()
|
const versions = new Set<string>()
|
||||||
|
|
||||||
for (const [key, meta] of Object.entries(packages)) {
|
for (const [key, meta] of Object.entries(packages)) {
|
||||||
const idx = key.lastIndexOf('node_modules/')
|
const idx = key.lastIndexOf('node_modules/')
|
||||||
const name = idx >= 0 ? key.slice(idx + 'node_modules/'.length) : key
|
const name = idx >= 0 ? key.slice(idx + 'node_modules/'.length) : key
|
||||||
|
|
||||||
if (name === TAP) {
|
if (name === TAP) {
|
||||||
if (meta.version) versions.add(meta.version)
|
if (meta.version) {versions.add(meta.version)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.ok(versions.size > 0, 'package-lock.json has no @assistant-ui/tap entry — the @assistant-ui cluster should resolve a single shared tap version.')
|
assert.ok(versions.size > 0, 'package-lock.json has no @assistant-ui/tap entry — the @assistant-ui cluster should resolve a single shared tap version.')
|
||||||
assert.ok(versions.size === 1, `@assistant-ui/tap resolves to multiple versions ${[...versions].sort()} — the cluster must share one tap line (see this test's docstring).`)
|
assert.ok(versions.size === 1, `@assistant-ui/tap resolves to multiple versions ${[...versions].sort()} — the cluster must share one tap line (see this test's docstring).`)
|
||||||
|
|
||||||
return [...versions][0]!
|
return [...versions][0]!
|
||||||
}
|
}
|
||||||
|
|
||||||
test('every @assistant-ui/* package\'s tap requirement is satisfiable', () => {
|
test('every @assistant-ui/* package\'s tap requirement is satisfiable', () => {
|
||||||
const packages = lockPackages()
|
const packages = lockPackages()
|
||||||
|
|
||||||
|
if (Object.keys(packages).length === 0) {return} // lockfile not materialized
|
||||||
|
|
||||||
const tapVersion = sharedTapVersion(packages)
|
const tapVersion = sharedTapVersion(packages)
|
||||||
|
|
||||||
const offenders: string[] = []
|
const offenders: string[] = []
|
||||||
|
|
||||||
for (const [key, meta] of Object.entries(packages)) {
|
for (const [key, meta] of Object.entries(packages)) {
|
||||||
const idx = key.lastIndexOf('node_modules/')
|
const idx = key.lastIndexOf('node_modules/')
|
||||||
const name = idx >= 0 ? key.slice(idx + 'node_modules/'.length) : key
|
const name = idx >= 0 ? key.slice(idx + 'node_modules/'.length) : key
|
||||||
if (!name.startsWith('@assistant-ui/') || name === TAP) continue
|
|
||||||
|
if (!name.startsWith('@assistant-ui/') || name === TAP) {continue}
|
||||||
const peerMeta = (meta.peerDependenciesMeta ?? {})[TAP]
|
const peerMeta = (meta.peerDependenciesMeta ?? {})[TAP]
|
||||||
if (peerMeta?.optional) continue
|
|
||||||
|
if (peerMeta?.optional) {continue}
|
||||||
const spec = (meta.dependencies ?? {})[TAP] || (meta.peerDependencies ?? {})[TAP]
|
const spec = (meta.dependencies ?? {})[TAP] || (meta.peerDependencies ?? {})[TAP]
|
||||||
if (!spec) continue
|
|
||||||
|
if (!spec) {continue}
|
||||||
|
|
||||||
if (!caretSatisfies(tapVersion, spec)) {
|
if (!caretSatisfies(tapVersion, spec)) {
|
||||||
offenders.push(`${name} requires ${TAP}"${spec}"`)
|
offenders.push(`${name} requires ${TAP}"${spec}"`)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import shared from '../eslint.config.shared.mjs'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...shared
|
||||||
|
]
|
||||||
|
|
@ -35,7 +35,7 @@ import path from 'node:path'
|
||||||
|
|
||||||
import { test } from 'vitest'
|
import { test } from 'vitest'
|
||||||
|
|
||||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..')
|
const REPO_ROOT = path.resolve(__dirname, '..')
|
||||||
const ROOT_PKG = path.join(REPO_ROOT, 'package.json')
|
const ROOT_PKG = path.join(REPO_ROOT, 'package.json')
|
||||||
const ROOT_LOCK = path.join(REPO_ROOT, 'package-lock.json')
|
const ROOT_LOCK = path.join(REPO_ROOT, 'package-lock.json')
|
||||||
|
|
||||||
|
|
@ -72,6 +72,7 @@ test('root lockfile has no camofox entries', () => {
|
||||||
// Some CI matrix shards skip lockfile materialization.
|
// Some CI matrix shards skip lockfile materialization.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = fs.readFileSync(ROOT_LOCK, 'utf-8')
|
const text = fs.readFileSync(ROOT_LOCK, 'utf-8')
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!text.includes('@askjo/camofox-browser'),
|
!text.includes('@askjo/camofox-browser'),
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "@hermes/root-tests",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
|
"test": "vitest run",
|
||||||
|
"check": "npm run typecheck && npm run test",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"fix": "npm run lint:fix"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^9.39.4",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"vitest": "^4.1.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
include: ['**/*.test.ts'],
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue