test: port workspace-level JS tests to a new js-tests workspace package

This commit is contained in:
ethernet 2026-07-15 12:35:19 -04:00
parent 2f3007ff51
commit 09f8a8268c
8 changed files with 90 additions and 11 deletions

12
package-lock.json generated
View file

@ -13,7 +13,8 @@
"apps/*",
"ui-tui",
"ui-tui/packages/*",
"web"
"web",
"tests-js"
],
"dependencies": {
"@streamdown/math": "^1.0.2",
@ -19678,6 +19679,15 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"tests-js": {
"name": "@hermes/root-tests",
"version": "0.0.0",
"dev": true
},
"node_modules/@hermes/root-tests": {
"resolved": "tests-js",
"link": true
}
}
}

View file

@ -7,7 +7,8 @@
"apps/*",
"ui-tui",
"ui-tui/packages/*",
"web"
"web",
"tests-js"
],
"scripts": {
"postinstall": "echo '✅ Browser tools ready. Run: python run_agent.py --help'",

View file

@ -37,7 +37,7 @@ import path from 'node:path'
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 TAP = '@assistant-ui/tap'
@ -51,18 +51,25 @@ function caretSatisfies(version: string, spec: string): boolean {
function parse(v: string): [number, number, number] {
const core = v.replace(/^[^0-9]+/, '').split('-')[0].split('+')[0]
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)]
}
const ver = parse(version)
for (const clause of spec.split('||')) {
const trimmed = clause.trim()
if (!trimmed) continue
if (!trimmed) {continue}
if (trimmed.startsWith('^')) {
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]
if (lo[0] > 0) {
hi = [lo[0] + 1, 0, 0]
} else if (lo[1] > 0) {
@ -70,6 +77,7 @@ function caretSatisfies(version: string, spec: string): boolean {
} else {
hi = [0, 0, lo[2] + 1]
}
if (
(ver[0] < hi[0]) ||
(ver[0] === hi[0] && ver[1] < hi[1]) ||
@ -83,6 +91,7 @@ function caretSatisfies(version: string, spec: string): boolean {
}
}
}
return false
}
@ -94,39 +103,52 @@ interface LockPackage {
}
function lockPackages(): Record<string, LockPackage> {
if (!fs.existsSync(LOCK_PATH)) {return {}}
const lock = JSON.parse(fs.readFileSync(LOCK_PATH, 'utf-8'))
return (lock.packages ?? {}) as Record<string, LockPackage>
}
function sharedTapVersion(packages: Record<string, LockPackage>): string {
/** The one tap version every install site resolves to. */
const versions = new Set<string>()
for (const [key, meta] of Object.entries(packages)) {
const idx = key.lastIndexOf('node_modules/')
const name = idx >= 0 ? key.slice(idx + 'node_modules/'.length) : key
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 === 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]!
}
test('every @assistant-ui/* package\'s tap requirement is satisfiable', () => {
const packages = lockPackages()
if (Object.keys(packages).length === 0) {return} // lockfile not materialized
const tapVersion = sharedTapVersion(packages)
const offenders: string[] = []
for (const [key, meta] of Object.entries(packages)) {
const idx = key.lastIndexOf('node_modules/')
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]
if (peerMeta?.optional) continue
if (peerMeta?.optional) {continue}
const spec = (meta.dependencies ?? {})[TAP] || (meta.peerDependencies ?? {})[TAP]
if (!spec) continue
if (!spec) {continue}
if (!caretSatisfies(tapVersion, spec)) {
offenders.push(`${name} requires ${TAP}"${spec}"`)
}

View file

@ -0,0 +1,5 @@
import shared from '../eslint.config.shared.mjs'
export default [
...shared
]

View file

@ -35,7 +35,7 @@ import path from 'node:path'
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_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.
return
}
const text = fs.readFileSync(ROOT_LOCK, 'utf-8')
assert.ok(
!text.includes('@askjo/camofox-browser'),

18
tests-js/package.json Normal file
View file

@ -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"
}
}

14
tests-js/tsconfig.json Normal file
View file

@ -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"]
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
include: ['**/*.test.ts'],
},
})