test(typert): run type-model.spec against an in-memory workspace

Each of the 61 cases copied the fixture onto disk and ran a full analysis
whose four TypeScript programs reloaded the DOM library, @types/node, and
zod v4; about 22,000 file-system probes per analysis dominated on a
network filesystem. The spec now mocks the node:fs calls the analyzer
makes and the ts.sys host for an in-memory workspace, stubs zod and
@types/node except in the Emitter case that runs real zod, drops the DOM
library from the fixture, and shares the pristine analysis read-only.
The file drops from about 316 s to 15 s with the same 61 cases and
assertions; the snapshot's external zod path no longer names a pnpm
store version.

Refs #5852
This commit is contained in:
imccyu committed 2026-10-12 11:06:52 +08:00
1 parent 989a52d427
commit 6fbfbb044e
4 files changed
+372 -42

No files matched your search

@@ -6450,7 +6450,7 @@ exports[`WorkspaceAnalyzer > builds independent face models with an explicit cro
],
"name": "ReexportedZodType",
"subpath": ".",
"symbol": "<external>:../../../../../../node_modules/.pnpm/[email protected]/node_modules/zod/v4/classic/schemas.d.cts#ZodType",
"symbol": "<external>:node_modules/zod/index.d.ts#ZodType",
},
],
"invocations": [],
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"lib": ["ES2024"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
@@ -0,0 +1,317 @@
/**
* In-memory fixture workspaces for `type-model.spec.ts`.
*
* The analyzer reads a workspace through `existsSync`, `readFileSync`, `realpathSync`, and `writeFileSync` from
* `node:fs`, and through `ts.sys`. A spec mocks `node:fs` with {@link memoryFileSystem} and calls
* {@link useMemoryWorkspaces}; paths under {@link MEMORY_ROOT} then live in this module's maps, and every other path
* reaches the real file system. Fixture copies, case edits, write-mode annotations, tsconfig expansion, and TypeScript
* module resolution inside a workspace never touch the disk.
*
* Each workspace mounts minimal `zod` and `@types/node` declaration stubs at its own `node_modules`, so analysis
* resolves neither the real packages nor anything outside the workspace except the TypeScript default libraries.
*
* This module must not import `node:fs`: the mock factory imports it while `node:fs` is being mocked.
*/
import { Buffer } from 'node:buffer'
import type * as NodeFileSystem from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import ts from 'typescript'
import { afterAll, afterEach, beforeAll } from 'vitest'
type FileSystem = typeof NodeFileSystem
interface FileSystemEntries {
readonly files: readonly string[]
readonly directories: readonly string[]
}
type MatchFiles = (
path: string,
extensions: readonly string[] | undefined,
excludes: readonly string[] | undefined,
includes: readonly string[] | undefined,
useCaseSensitiveFileNames: boolean,
currentDirectory: string,
depth: number | undefined,
getFileSystemEntries: (path: string) => FileSystemEntries,
realpath: (path: string) => string,
) => string[]
/** TypeScript's include/exclude matcher, the one `ts.sys.readDirectory` applies to real directory listings. */
const { matchFiles } = ts as typeof ts & { readonly matchFiles: MatchFiles | undefined }
/** Absolute directory that holds every in-memory workspace; it does not exist on disk. */
export const MEMORY_ROOT = resolve('/dsh-typert-memory')
/** The unmodified fixture workspace on disk, read once per spec file. */
export const fixtureRoot = resolve(import.meta.dirname, 'fixtures/type-model')
/** Declaration stubs for the fixture's external packages, mounted under each workspace's `node_modules`. */
const DEPENDENCY_STUBS: Readonly<Record<string, string>> = {
'zod/package.json': `${JSON.stringify({ name: 'zod', version: '0.0.0', type: 'module', types: './index.d.ts' })}\n`,
'zod/index.d.ts': 'export interface ZodType<Output = unknown> { readonly _output: Output }\n',
'@types/node/package.json': `${JSON.stringify({ name: '@types/node', version: '0.0.0', types: './index.d.ts' })}\n`,
'@types/node/index.d.ts': 'declare namespace NodeJS { interface Process { readonly pid: number } }\n',
}
const memoryRootKey = key(MEMORY_ROOT)
const files = new Map<string, string>()
const directories = new Set<string>()
const readOnlyRoots = new Set<string>()
const caseRoots: string[] = []
let realFileSystem: FileSystem | undefined
let fixtureFiles: ReadonlyMap<string, string> | undefined
let pristineRoot: string | undefined
let sequence = 0
function key(path: string): string {
return resolve(path).replaceAll('\\', '/')
}
function isMemoryPath(path: NodeFileSystem.PathOrFileDescriptor): path is string {
if (typeof path !== 'string') return false
const normalized = key(path)
return normalized === memoryRootKey || normalized.startsWith(`${memoryRootKey}/`)
}
function isWithin(target: string, root: string): boolean {
return target === root || target.startsWith(`${root}/`)
}
function fileSystemError(code: 'EEXIST' | 'EISDIR' | 'ENOENT' | 'EROFS', syscall: string, path: string): NodeJS.ErrnoException {
return Object.assign(new Error(`${code}: ${syscall} '${path}'`), { code, syscall, path })
}
function addDirectory(path: string): void {
let current = key(path)
while (!directories.has(current)) {
directories.add(current)
if (current === memoryRootKey) return
current = key(dirname(current))
}
}
function setFile(path: string, content: string): void {
addDirectory(dirname(path))
files.set(key(path), content)
}
function assertWritable(target: string, syscall: string, path: string): void {
for (const root of readOnlyRoots) {
if (isWithin(target, root)) throw fileSystemError('EROFS', syscall, path)
}
}
function writeMemoryFile(path: string, content: string): void {
const target = key(path)
assertWritable(target, 'open', path)
if (directories.has(target)) throw fileSystemError('EISDIR', 'open', path)
if (!directories.has(key(dirname(path)))) throw fileSystemError('ENOENT', 'open', path)
files.set(target, content)
}
function makeMemoryDirectory(path: string, recursive: boolean): string | undefined {
const target = key(path)
assertWritable(target, 'mkdir', path)
if (directories.has(target)) {
if (recursive) return undefined
throw fileSystemError('EEXIST', 'mkdir', path)
}
if (files.has(target)) throw fileSystemError('EEXIST', 'mkdir', path)
if (!recursive && !directories.has(key(dirname(path)))) throw fileSystemError('ENOENT', 'mkdir', path)
let first = target
for (let parent = key(dirname(first)); !directories.has(parent); parent = key(dirname(parent))) first = parent
addDirectory(target)
return recursive ? resolve(first) : undefined
}
function memoryEntries(path: string): FileSystemEntries {
const prefix = `${key(path)}/`
const children = (entries: Iterable<string>): string[] => [...entries]
.flatMap(entry => entry.startsWith(prefix) && !entry.slice(prefix.length).includes('/') ? [entry.slice(prefix.length)] : [])
.sort()
return { files: children(files.keys()), directories: children(directories) }
}
function removeTree(root: string): void {
for (const file of [...files.keys()]) if (isWithin(file, root)) files.delete(file)
for (const directory of [...directories]) if (isWithin(directory, root)) directories.delete(directory)
}
/**
* Build the `node:fs` mock for `vi.mock('node:fs', ...)`. Memory paths are served from this module; every other
* path, and every other `node:fs` export, is the real implementation.
* @param real - the actual `node:fs` module from `importOriginal`.
* @returns the mocked module namespace, including its `default` export.
*/
export function memoryFileSystem(real: FileSystem): Record<string, unknown> {
realFileSystem = real
const memory = {
existsSync(path: NodeFileSystem.PathLike): boolean {
if (!isMemoryPath(path)) return real.existsSync(path)
return files.has(key(path)) || directories.has(key(path))
},
readFileSync(
path: NodeFileSystem.PathOrFileDescriptor,
options?: BufferEncoding | { readonly encoding?: BufferEncoding | null; readonly flag?: string } | null,
): string | Buffer {
if (!isMemoryPath(path)) return real.readFileSync(path, options)
const content = files.get(key(path))
if (content === undefined) throw fileSystemError(directories.has(key(path)) ? 'EISDIR' : 'ENOENT', 'open', path)
const encoding = typeof options === 'string' ? options : options?.encoding
return encoding === undefined || encoding === null ? Buffer.from(content) : Buffer.from(content).toString(encoding)
},
writeFileSync(
path: NodeFileSystem.PathOrFileDescriptor,
data: string | NodeJS.ArrayBufferView,
options?: NodeFileSystem.WriteFileOptions,
): void {
if (!isMemoryPath(path)) {
real.writeFileSync(path, data, options)
return
}
writeMemoryFile(path, typeof data === 'string'
? data
: Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8'))
},
mkdirSync(
path: NodeFileSystem.PathLike,
options?: NodeFileSystem.Mode | NodeFileSystem.MakeDirectoryOptions | null,
): string | undefined {
if (!isMemoryPath(path)) return real.mkdirSync(path, options)
return makeMemoryDirectory(path, typeof options === 'object' && options?.recursive === true)
},
realpathSync(path: NodeFileSystem.PathLike, options?: NodeFileSystem.EncodingOption): string {
if (!isMemoryPath(path)) return real.realpathSync(path, options)
if (!files.has(key(path)) && !directories.has(key(path))) throw fileSystemError('ENOENT', 'realpath', path)
return resolve(path)
},
}
return { ...real, ...memory, default: { ...real, ...memory } }
}
function installMemorySystem(): () => void {
if (matchFiles === undefined) throw new Error('typescript no longer exposes matchFiles for in-memory readDirectory')
const match = matchFiles
const system = ts.sys
/* oxlint-disable typescript/unbound-method -- Each saved method is invoked with `.call(system)` or restored onto `system`. */
const original = {
fileExists: system.fileExists,
directoryExists: system.directoryExists,
getDirectories: system.getDirectories,
readDirectory: system.readDirectory,
readFile: system.readFile,
realpath: system.realpath,
}
/* oxlint-enable typescript/unbound-method */
// Every checkProject program re-reads the default libraries from the installed typescript package, which is immutable.
const libraryRoot = key(dirname(ts.getDefaultLibFilePath({})))
const libraryFiles = new Map<string, string | undefined>()
system.fileExists = path => isMemoryPath(path) ? files.has(key(path)) : original.fileExists.call(system, path)
system.directoryExists = path => isMemoryPath(path)
? directories.has(key(path))
: original.directoryExists.call(system, path)
system.getDirectories = path => isMemoryPath(path)
? [...memoryEntries(path).directories]
: original.getDirectories.call(system, path)
system.readDirectory = (path, extensions, excludes, includes, depth) => isMemoryPath(path)
? match(path, extensions, excludes, includes, system.useCaseSensitiveFileNames, process.cwd(), depth, memoryEntries, entry => entry)
: original.readDirectory.call(system, path, extensions, excludes, includes, depth)
system.readFile = (path, encoding) => {
if (isMemoryPath(path)) return files.get(key(path))
if (encoding !== undefined || !isWithin(key(path), libraryRoot)) return original.readFile.call(system, path, encoding)
if (!libraryFiles.has(path)) libraryFiles.set(path, original.readFile.call(system, path))
return libraryFiles.get(path)
}
const { realpath } = original
if (realpath !== undefined) system.realpath = path => isMemoryPath(path) ? path : realpath.call(system, path)
return () => { Object.assign(system, original) }
}
/**
* Register the spec-file hooks that own memory workspaces: `ts.sys` routes memory paths from `beforeAll` until
* `afterAll`, and the workspaces a case creates are dropped after it.
*/
export function useMemoryWorkspaces(): void {
let restore: (() => void) | undefined
beforeAll(() => { restore = installMemorySystem() })
afterEach(() => {
for (const root of caseRoots.splice(0)) removeTree(root)
})
afterAll(() => {
restore?.()
files.clear()
directories.clear()
readOnlyRoots.clear()
pristineRoot = undefined
})
}
function fixtureSources(): ReadonlyMap<string, string> {
if (fixtureFiles !== undefined) return fixtureFiles
const real = realFileSystem
if (real === undefined) throw new Error('mock node:fs with memoryFileSystem before reading the fixture')
const collected = new Map<string, string>()
const visit = (directory: string, relativeDirectory: string): void => {
for (const entry of real.readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name)
const relativePath = join(relativeDirectory, entry.name)
if (entry.isDirectory()) visit(path, relativePath)
else collected.set(relativePath, real.readFileSync(path, 'utf8'))
}
}
visit(fixtureRoot, '')
fixtureFiles = collected
return collected
}
function mountDependencyStubs(root: string): void {
for (const [path, content] of Object.entries(DEPENDENCY_STUBS)) setFile(join(root, 'node_modules', path), content)
}
function mountFixture(root: string): void {
for (const [path, content] of fixtureSources()) setFile(join(root, path), content)
mountDependencyStubs(root)
}
/**
* Create an empty memory workspace owned by the current case.
* @param prefix - directory-name prefix that identifies the owning case.
* @param options - `dependencies` mounts the `zod` and `@types/node` stubs under its `node_modules`.
* @returns the absolute workspace root.
*/
export function emptyWorkspace(prefix: string, options: { readonly dependencies?: boolean } = {}): string {
sequence += 1
const root = join(MEMORY_ROOT, `${prefix}${String(sequence)}`)
addDirectory(root)
caseRoots.push(key(root))
if (options.dependencies === true) mountDependencyStubs(root)
return root
}
/**
* Create an editable memory copy of the fixture workspace owned by the current case.
* @param prefix - directory-name prefix that identifies the owning case.
* @returns the absolute workspace root.
*/
export function copyFixture(prefix: string): string {
const root = emptyWorkspace(prefix)
mountFixture(root)
return root
}
/**
* Return the read-only memory copy of the fixture shared by every case of the spec file; writes under it throw
* `EROFS`.
* @returns the absolute workspace root.
*/
export function pristineFixture(): string {
if (pristineRoot !== undefined) return pristineRoot
const root = join(MEMORY_ROOT, 'pristine')
addDirectory(root)
mountFixture(root)
readOnlyRoots.add(key(root))
pristineRoot = root
return root
}
@@ -1,8 +1,8 @@
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import ts from 'typescript'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { describe, expect, it, onTestFinished, vi } from 'vitest'
import { TypertAnalysisError, WorkspaceAnalyzer, WorkspaceCaches } from '../src/analyzer.ts'
import { FaceModelEmitter } from '../src/emitter.ts'
import type {
@@ -12,22 +12,28 @@ import type {
TypeNodeModel,
TypeOperatorName,
TypeTargetModel,
WorkspaceModel,
} from '../src/model.ts'
import { TypeGraphRenderer } from '../src/renderer.ts'
import { WorkspaceTypertGenerator } from '../src/workspace.ts'
import { copyFixture, emptyWorkspace, pristineFixture, useMemoryWorkspaces } from './type-model-workspace.ts'
const fixtureRoot = resolve(import.meta.dirname, 'fixtures/type-model')
const temporaryRoots: string[] = []
vi.mock('node:fs', async (importOriginal) => {
const { memoryFileSystem } = await import('./type-model-workspace.ts')
return memoryFileSystem(await importOriginal<typeof import('node:fs')>())
})
function normalizedPath(path: string): string {
return path.replaceAll('\\', '/')
}
const parseConfigHost: ts.ParseConfigFileHost = {
...ts.sys,
onUnRecoverableConfigFileDiagnostic(diagnostic) {
throw new Error(formatDiagnostic(diagnostic))
},
function parseConfigHost(): ts.ParseConfigFileHost {
return {
...ts.sys,
onUnRecoverableConfigFileDiagnostic(diagnostic) {
throw new Error(formatDiagnostic(diagnostic))
},
}
}
const TYPE_NODE_KINDS = {
@@ -99,13 +105,25 @@ const MEMBER_KINDS = {
index: true,
} as const satisfies Record<MemberModel['kind'], true>
afterEach(() => {
for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true })
})
useMemoryWorkspaces()
let fixtureModel: WorkspaceModel | undefined
/** The default analysis of the pristine fixture, computed once and deeply frozen so no case can alter what another reads. */
function analyzedFixture(): WorkspaceModel {
fixtureModel ??= deepFreeze(new WorkspaceAnalyzer({ root: pristineFixture() }).analyze())
return fixtureModel
}
function deepFreeze<Value>(value: Value): Value {
if (typeof value !== 'object' || value === null || Object.isFrozen(value)) return value
for (const child of Object.values(value)) deepFreeze(child)
return Object.freeze(value)
}
describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
it('builds independent face models with an explicit cross-face type graph', () => {
const model = new WorkspaceAnalyzer({ root: fixtureRoot }).analyze()
const model = analyzedFixture()
expect(model.faces.map(face => face.face)).toEqual(['host', 'client'])
expect(model.crossFaceLinks).toContainEqual({
@@ -199,7 +217,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
it('merges bounded package programs into the same face model', () => {
const options = {
root: fixtureRoot,
root: pristineFixture(),
packages: ['@fixture/host', '@fixture/client'],
} as const
const direct = new WorkspaceAnalyzer(options).analyze()
@@ -256,7 +274,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
})
it('indexes authored top-level exports without promoting them to graph roots', () => {
const declarations = new WorkspaceAnalyzer({ root: fixtureRoot }).indexSourceDeclarations()
const declarations = new WorkspaceAnalyzer({ root: pristineFixture() }).indexSourceDeclarations()
const agent = declarations.find(declaration => declaration.name === 'Agent')
expect(agent).toMatchObject({
@@ -271,7 +289,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
})
it('covers every modeled discriminant with source-authored fixture syntax', () => {
const model = new WorkspaceAnalyzer({ root: fixtureRoot }).analyze()
const model = analyzedFixture()
const nodes = model.faces.flatMap(face => face.graph.nodes)
const declarations = model.faces.flatMap(face => face.graph.declarations)
const members = declarations.flatMap(declaration => declaration.members)
@@ -935,7 +953,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
config.compilerOptions ??= {}
config.compilerOptions.typeRoots = [
configPath === packageConfigPath ? '../../node_modules' : './node_modules',
resolve('node_modules/@types'),
join(root, 'node_modules/@types'),
]
config.compilerOptions.types = ['unscoped-global', 'node']
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
@@ -950,12 +968,12 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
const packageConfig = ts.getParsedCommandLineOfConfigFile(
join(root, 'packages/host/tsconfig.json'),
{},
parseConfigHost,
parseConfigHost(),
) as ts.ParsedCommandLine
const aggregateConfig = ts.getParsedCommandLineOfConfigFile(
join(root, 'tsconfig.host.json'),
{},
parseConfigHost,
parseConfigHost(),
) as ts.ParsedCommandLine
const diagnosticProgram = ts.createProgram({
rootNames: packageConfig.fileNames,
@@ -1027,8 +1045,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
})
it('handles empty selections and rejects malformed aggregate configs', () => {
const empty = mkdtempSync(join(import.meta.dirname, '.typert-empty-workspace-'))
temporaryRoots.push(empty)
const empty = emptyWorkspace('typert-empty-workspace-')
expect(new WorkspaceAnalyzer({ root: empty }).analyze()).toEqual({ faces: [], crossFaceLinks: [] })
writeFileSync(join(empty, 'empty.d.ts'), 'export {}\n')
@@ -1041,7 +1058,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
writeFileSync(join(empty, 'tsconfig.host.json'), JSON.stringify({ compilerOptions: { target: 'invalid' } }))
expect(() => new WorkspaceAnalyzer({ root: empty }).analyze()).toThrow(TypertAnalysisError)
expect(new WorkspaceAnalyzer({ root: fixtureRoot, packages: ['@fixture/absent'] }).analyze())
expect(new WorkspaceAnalyzer({ root: pristineFixture(), packages: ['@fixture/absent'] }).analyze())
.toEqual({ faces: [], crossFaceLinks: [] })
})
@@ -1071,8 +1088,7 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
})
it('reports an aggregate without references or inputs', () => {
const root = mkdtempSync(join(import.meta.dirname, '.typert-aggregate-no-inputs-'))
temporaryRoots.push(root)
const root = emptyWorkspace('typert-aggregate-no-inputs-')
writeFileSync(join(root, 'tsconfig.host.json'), '{ "include": ["missing"] }\n')
expect(() => new WorkspaceAnalyzer({ root }).discoverPackages()).toThrow('No inputs were found')
@@ -1311,13 +1327,13 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
describe('TypeGraphRenderer', { timeout: 60_000 }, () => {
it('retains every source-authored SyntaxZoo property type through rendering', () => {
const host = new WorkspaceAnalyzer({ root: fixtureRoot }).analyze().faces
const host = analyzedFixture().faces
.find(face => face.face === 'host')
if (host === undefined) throw new Error('fixture has no host face')
const declaration = host.graph.declarations.find(candidate => candidate.name === 'SyntaxZoo')
if (declaration === undefined) throw new Error('fixture has no SyntaxZoo declaration')
const sourcePath = join(fixtureRoot, 'packages/host/src/models.ts')
const sourcePath = join(pristineFixture(), 'packages/host/src/models.ts')
const source = ts.createSourceFile(
sourcePath,
readFileSync(sourcePath, 'utf8'),
@@ -1347,9 +1363,8 @@ describe('TypeGraphRenderer', { timeout: 60_000 }, () => {
})
it('renders every analyzed declaration as compilable TypeScript', () => {
const model = new WorkspaceAnalyzer({ root: fixtureRoot }).analyze()
const root = mkdtempSync(join(import.meta.dirname, '.rendered-model-'))
temporaryRoots.push(root)
const model = analyzedFixture()
const root = emptyWorkspace('rendered-model-', { dependencies: true })
const externalTypes = join(root, 'external.d.ts')
writeFileSync(externalTypes, [
'declare module \'@fixture/host\' {',
@@ -1394,6 +1409,8 @@ describe('TypeGraphRenderer', { timeout: 60_000 }, () => {
noEmit: true,
skipLibCheck: false,
target: ts.ScriptTarget.ES2024,
lib: ['lib.es2024.d.ts'],
types: [],
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
},
@@ -1404,7 +1421,7 @@ describe('TypeGraphRenderer', { timeout: 60_000 }, () => {
describe('WorkspaceTypertGenerator', { timeout: 60_000 }, () => {
it('emits host and client faces through their exact root-level public artifacts', () => {
const artifacts = new WorkspaceTypertGenerator(fixtureRoot).generate()
const artifacts = new WorkspaceTypertGenerator(pristineFixture()).generate()
expect(artifacts.map(artifact => ({ package: artifact.package, face: artifact.face }))).toEqual([
{ package: '@fixture/host', face: 'host' },
{ package: '@fixture/client', face: 'client' },
@@ -1499,13 +1516,6 @@ function canonicalType(text: string): string {
return printType(declaration.type, source)
}
function copyFixture(prefix: string): string {
const root = mkdtempSync(join(import.meta.dirname, `.${prefix}`))
temporaryRoots.push(root)
cpSync(fixtureRoot, root, { recursive: true })
return root
}
function configureDualRuntimeClient(root: string, splitProjects: boolean): void {
const packageRoot = join(root, 'packages/client')
const manifestPath = join(packageRoot, 'package.json')
@@ -1654,16 +1664,16 @@ function addExplicitServicePackage(root: string, annotation: string, withProtoco
describe('FaceModelEmitter', { timeout: 60_000 }, () => {
it('emits runnable Zod JavaScript, precise declarations, and runtime package metadata', async () => {
const model = new WorkspaceAnalyzer({ root: fixtureRoot }).analyze()
const host = model.faces.find(face => face.face === 'host')
const host = analyzedFixture().faces.find(face => face.face === 'host')
if (host === undefined) throw new Error('fixture has no host face')
const artifact = new FaceModelEmitter(host).emit('@fixture/host')
expect(artifact.js).toMatchSnapshot()
expect(artifact.dts).toMatchSnapshot()
// The one case that runs generated code against the real zod package resolves it from beside this directory.
const root = mkdtempSync(join(import.meta.dirname, '.generated-model-'))
temporaryRoots.push(root)
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
const modulePath = join(root, 'host.mjs')
writeFileSync(modulePath, artifact.js)
const generated = await import(`${pathToFileURL(modulePath).href}?test=${Date.now()}`) as {
@@ -1711,6 +1721,8 @@ describe('FaceModelEmitter', { timeout: 60_000 }, () => {
noEmit: true,
skipLibCheck: false,
target: ts.ScriptTarget.ES2024,
lib: ['lib.es2024.d.ts'],
types: [],
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
},