Focus configuration tests on application behavior #50

Merged
eleith merged 1 commits from test/focused-config-tests into main 2026-10-26 02:30:12 +00:00
7 changed files with 11 additions and 249 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
'@directory/web': patch
'@directory/cli': patch
---
Remove config build tests and check defaults through the loader.
+1 -1
View File
@@ -49,5 +49,5 @@ jobs:
run: pnpm --filter @directory/config check
- name: Lint repository
run: pnpm lint
- name: Unit and artifact tests
- name: Unit tests
run: pnpm --filter @directory/config test:coverage
@@ -1,184 +0,0 @@
import { spawnSync } from 'node:child_process';
import {
cp,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
stat,
symlink,
writeFile
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
const packageDirectory = fileURLToPath(new URL('..', import.meta.url));
const workspaceDirectory = fileURLToPath(new URL('../../..', import.meta.url));
let fixtureRoot: string;
let fixturePackage: string;
let artifact: string;
function run(...args: string[]) {
const result = spawnSync(
process.execPath,
[join(fixturePackage, 'scripts/generate-schema.ts'), ...args],
{ cwd: fixtureRoot, encoding: 'utf8', timeout: 30_000, maxBuffer: 2 * 1024 * 1024 }
);
expect(result.error).toBeUndefined();
return result;
}
async function expectCleanCache(): Promise<void> {
const entries = await readdir(join(fixturePackage, 'node_modules/.cache'));
expect(entries.filter((name) => name.startsWith('directory-schema-'))).toEqual([]);
}
beforeEach(async () => {
fixtureRoot = await mkdtemp(join(tmpdir(), 'directory-schema-test-'));
fixturePackage = join(fixtureRoot, 'packages/config');
artifact = join(fixturePackage, 'src/config.schema.json');
await mkdir(join(fixturePackage, 'scripts'), { recursive: true });
await cp(join(workspaceDirectory, 'tsconfig.base.json'), join(fixtureRoot, 'tsconfig.base.json'));
for (const name of ['package.json', 'tsconfig.json', 'src']) {
await cp(join(packageDirectory, name), join(fixturePackage, name), { recursive: true });
}
await cp(
join(packageDirectory, 'scripts/generate-schema.ts'),
join(fixturePackage, 'scripts/generate-schema.ts')
);
await rm(artifact, { force: true });
await symlink(join(workspaceDirectory, 'node_modules'), join(fixtureRoot, 'node_modules'), 'dir');
// Link dependencies individually so the generator's cache stays disposable, too.
await mkdir(join(fixturePackage, 'node_modules'));
for (const name of await readdir(join(packageDirectory, 'node_modules'))) {
if (!name.startsWith('.')) {
await symlink(
join(packageDirectory, 'node_modules', name),
join(fixturePackage, 'node_modules', name),
'dir'
);
}
}
});
afterEach(async () => {
if (fixtureRoot) {
await rm(fixtureRoot, { recursive: true, force: true });
}
});
describe('schema generator CLI from clean source', () => {
it('generates before a build, formats deterministically, and checks without rewriting', async () => {
expect(run().status).toBe(0);
const first = await readFile(artifact, 'utf8');
expect(JSON.parse(first)).toMatchObject({
$schema: 'https://json-schema.org/draft/2020-12/schema'
});
expect(first.endsWith('\n')).toBe(true);
expect(first).toContain('\n\t');
expect(run().status).toBe(0);
expect(await readFile(artifact, 'utf8')).toBe(first);
const beforeCheck = await stat(artifact, { bigint: true });
const checked = run('--check');
expect(checked.status, checked.stderr).toBe(0);
expect(checked.stdout).toContain('up to date');
expect(await readFile(artifact, 'utf8')).toBe(first);
const afterCheck = await stat(artifact, { bigint: true });
expect(afterCheck.mtimeNs).toBe(beforeCheck.mtimeNs);
expect(afterCheck.ctimeNs).toBe(beforeCheck.ctimeNs);
await expectCleanCache();
}, 30_000);
it('fails on byte drift without changing the artifact', async () => {
expect(run().status).toBe(0);
const drifted = `${await readFile(artifact, 'utf8')}\n`;
await writeFile(artifact, drifted);
const checked = run('--check');
expect(checked.status).toBe(1);
expect(checked.stderr).toContain('out of date');
expect(checked.stderr).toContain('pnpm schema:generate');
expect(await readFile(artifact, 'utf8')).toBe(drifted);
await expectCleanCache();
}, 30_000);
it('reports a missing artifact without generating it', async () => {
const checked = run('--check');
expect(checked.status).toBe(1);
expect(checked.stderr).toContain('src/config.schema.json is missing');
await expect(readFile(artifact)).rejects.toMatchObject({ code: 'ENOENT' });
await expectCleanCache();
}, 30_000);
it('uses edited canonical source even when dist contains stale output', async () => {
const dist = join(fixturePackage, 'dist');
await mkdir(dist);
const staleModule = 'throw new Error("Stale dist must not be imported");\n';
for (const name of ['index.js', 'schema.js', 'editor-schema.js']) {
await writeFile(join(dist, name), staleModule);
}
expect(run().status).toBe(0);
const original = await readFile(artifact, 'utf8');
const schemaSource = join(fixturePackage, 'src/schema.ts');
await writeFile(
schemaSource,
`${await readFile(schemaSource, 'utf8')}\nDirectoryConfigurationSchema.description = 'Fresh canonical source fixture';\n`
);
const checked = run('--check');
expect(checked.status, checked.stderr).toBe(1);
expect(checked.stderr).toContain('out of date');
expect(await readFile(artifact, 'utf8')).toBe(original);
const generated = run();
expect(generated.status, generated.stderr).toBe(0);
const updated = await readFile(artifact, 'utf8');
expect(updated).not.toBe(original);
expect(updated).toContain('Fresh canonical source fixture');
for (const name of ['index.js', 'schema.js', 'editor-schema.js']) {
expect(await readFile(join(dist, name), 'utf8')).toBe(staleModule);
}
await expectCleanCache();
}, 30_000);
it('rejects unknown and repeated arguments without generating an artifact', async () => {
for (const args of [['--unknown'], ['--check', '--check'], ['--check', 'extra'], ['--']]) {
const result = run(...args);
expect(result.status).toBe(1);
expect(result.stderr).toContain('Usage: node scripts/generate-schema.ts [--check]');
}
await expect(readFile(artifact)).rejects.toMatchObject({ code: 'ENOENT' });
await expect(readdir(join(fixturePackage, 'node_modules/.cache'))).rejects.toMatchObject({
code: 'ENOENT'
});
}, 30_000);
it('reports compilation errors, preserves the artifact, and removes temporary output', async () => {
await writeFile(artifact, 'artifact sentinel\n');
const schemaSource = join(fixturePackage, 'src/schema.ts');
await writeFile(
schemaSource,
`${await readFile(schemaSource, 'utf8')}\nconst compilationFailure: string = 123;\n`
);
const result = run();
expect(result.status).toBe(1);
expect(result.stderr).toContain('Schema generation failed:');
expect(result.stderr).toContain('TS2322');
expect(await readFile(artifact, 'utf8')).toBe('artifact sentinel\n');
await expectCleanCache();
}, 30_000);
it('cleans temporary modules after an import failure without writing an artifact', async () => {
const editorSource = join(fixturePackage, 'src/editor-schema.ts');
await writeFile(
editorSource,
`${await readFile(editorSource, 'utf8')}\nthrow new Error('Fixture import failed');\n`
);
const result = run();
expect(result.status).toBe(1);
expect(result.stderr).toContain('Fixture import failed');
await expect(readFile(artifact)).rejects.toMatchObject({ code: 'ENOENT' });
await expectCleanCache();
}, 30_000);
});
-47
View File
@@ -1,47 +0,0 @@
import { readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { configurationVersion, DirectoryConfigurationSchema } from '@directory/config';
import type { DirectoryConfiguration, DirectorySite } from '@directory/config/types';
import { describe, expect, expectTypeOf, it } from 'vitest';
describe('@directory/config compiled public module', () => {
it('exports the supported configuration format as a literal', () => {
expect(configurationVersion).toBe(1);
expectTypeOf<typeof configurationVersion>().toEqualTypeOf<1>();
expect(import.meta.resolve('@directory/config')).toBe(
new URL('../dist/index.js', import.meta.url).href
);
});
it('exports the canonical schema and editor JSON at their compiled/artifact paths', () => {
expect(DirectoryConfigurationSchema.properties.version.const).toBe(configurationVersion);
expect(import.meta.resolve('@directory/config/schema.json')).toBe(
new URL('./config.schema.json', import.meta.url).href
);
expectTypeOf<DirectoryConfiguration['version']>().toEqualTypeOf<1>();
expectTypeOf<DirectorySite['logo_path']>().toEqualTypeOf<string>();
expectTypeOf<DirectorySite['favicon_path']>().toEqualTypeOf<string>();
expectTypeOf<DirectorySite['open_links_in_new_tab']>().toEqualTypeOf<boolean>();
});
it('keeps the types entry free of runtime schema, format registration and loader imports', () => {
const typesUrl = import.meta.resolve('@directory/config/types');
expect(typesUrl).toBe(new URL('../dist/types.js', import.meta.url).href);
expect(readFileSync(new URL(typesUrl), 'utf8').trim()).toBe('export {};');
const result = spawnSync(
process.execPath,
[
'--input-type=module',
'--eval',
`
const types = await import(${JSON.stringify(typesUrl)});
const { FormatRegistry } = await import(${JSON.stringify(import.meta.resolve('@sinclair/typebox'))});
console.log(JSON.stringify({ exports: Object.keys(types), registered: FormatRegistry.Has('directory-http-url') }));
`
],
{ encoding: 'utf8' }
);
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({ exports: [], registered: false });
});
});
+3 -1
View File
@@ -51,7 +51,7 @@ function expectRedacted(error: unknown): void {
}
describe('default, interpolate, then validate', () => {
it('loads defaults while retaining authored order, tag lists, group and advisory flags', () => {
it('loads independent defaults while preserving authored values and order', () => {
const raw = {
links: {
zebra: {
@@ -96,6 +96,8 @@ describe('default, interpolate, then validate', () => {
});
expect(config).not.toHaveProperty('auth');
expect(config.links.alpha).not.toHaveProperty('group');
config.links.alpha!.tags.push('changed');
expect(validateConfig(raw, { env: {} }).links.alpha!.tags).toEqual([]);
expect(raw).toEqual(before);
});
-15
View File
@@ -82,21 +82,6 @@ describe('configuration defaults and inferred types', () => {
expect(value).not.toHaveProperty('links.router.group');
});
it('does not mutate authored input or share mutable defaults between calls', () => {
const first = defaultFixture(raw);
if (!Value.Check(DirectoryConfigurationSchema, configurationReferences, first))
throw new Error('Invalid fixture');
first.site.title = 'Changed';
first.site.logo_path = '/public/changed.svg';
expect(defaultFixture(raw)).toHaveProperty('site.logo_path', '/favicon.svg');
first.links['photos']!.requires_auth = true;
first.links['photos']!.tags.push('changed');
expect(defaultFixture(raw)).toHaveProperty('links.photos.tags', []);
expect(defaultFixture(raw)).toHaveProperty('site.title', 'Directory');
expect(defaultFixture(raw)).toHaveProperty('links.photos.requires_auth', false);
expect(raw).toEqual({ links: { photos: link } });
});
it('keeps loaded defaults required and genuinely optional metadata optional in TypeScript', () => {
expectTypeOf<DirectoryConfiguration['version']>().toEqualTypeOf<1>();
expectTypeOf<DirectoryConfiguration['site']['title']>().toEqualTypeOf<string>();
+1 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['src/**/*.test.ts', 'scripts/**/*.test.ts'],
include: ['src/**/*.test.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],