From 337f2a761dacbc5d576c5272d6ef9ff1d8e35c11 Mon Sep 17 00:00:00 2001 From: Jack-sh1 Date: Wed, 13 May 2026 18:56:25 +0800 Subject: [PATCH 1/3] test: add unit tests for build adapter createBuild had zero test coverage. Add tests for output directory creation/cleanup, SPA dist copying, __connection.json generation, __rpc-dump directory and manifest writing, spa-loader.json conditional output, and setup hook invocation. --- packages/devframe/src/adapters/build.test.ts | 98 ++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/devframe/src/adapters/build.test.ts diff --git a/packages/devframe/src/adapters/build.test.ts b/packages/devframe/src/adapters/build.test.ts new file mode 100644 index 0000000..837b120 --- /dev/null +++ b/packages/devframe/src/adapters/build.test.ts @@ -0,0 +1,98 @@ +import type { DevframeDefinition } from '../types/devframe' +import { existsSync } from 'node:fs' +import fs from 'node:fs/promises' +import { resolve } from 'pathe' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createBuild } from './build' + +// Minimal stub definition +function stubDefinition(overrides: Partial = {}): DevframeDefinition { + return { + id: 'test-devframe', + setup: vi.fn(async () => {}), + cli: { distDir: '/tmp/test-spa-dist' }, + ...overrides, + } as any +} + +describe('createBuild', () => { + const outDir = resolve('/tmp/devframe-build-test-out') + const distDir = resolve('/tmp/test-spa-dist') + + beforeEach(async () => { + // Create a fake SPA dist directory with an index.html + await fs.mkdir(distDir, { recursive: true }) + await fs.writeFile(resolve(distDir, 'index.html'), '') + }) + + afterEach(async () => { + await fs.rm(outDir, { recursive: true, force: true }) + await fs.rm(distDir, { recursive: true, force: true }) + }) + + it('throws when no distDir is provided', async () => { + const d = stubDefinition({ cli: undefined }) + await expect(createBuild(d, { outDir })).rejects.toThrow('no distDir') + }) + + it('creates output directory', async () => { + const d = stubDefinition() + await createBuild(d, { outDir, distDir }) + expect(existsSync(outDir)).toBe(true) + }) + + it('copies SPA dist into outDir', async () => { + const d = stubDefinition() + await createBuild(d, { outDir, distDir }) + expect(existsSync(resolve(outDir, 'index.html'))).toBe(true) + }) + + it('writes __connection.json with backend: static', async () => { + const d = stubDefinition() + await createBuild(d, { outDir, distDir }) + const meta = JSON.parse(await fs.readFile(resolve(outDir, '__connection.json'), 'utf-8')) + expect(meta.backend).toBe('static') + }) + + it('creates __rpc-dump directory', async () => { + const d = stubDefinition() + await createBuild(d, { outDir, distDir }) + expect(existsSync(resolve(outDir, '__rpc-dump'))).toBe(true) + }) + + it('writes __rpc-dump/index.json manifest', async () => { + const d = stubDefinition() + await createBuild(d, { outDir, distDir }) + expect(existsSync(resolve(outDir, '__rpc-dump/index.json'))).toBe(true) + }) + + it('writes spa-loader.json when d.spa is defined', async () => { + const d = stubDefinition({ spa: { loader: 'query' } } as any) + await createBuild(d, { outDir, distDir, base: '/app/' }) + const loader = JSON.parse(await fs.readFile(resolve(outDir, 'spa-loader.json'), 'utf-8')) + expect(loader.version).toBe(1) + expect(loader.mode).toBe('query') + expect(loader.base).toBe('/app/') + }) + + it('does not write spa-loader.json when d.spa is undefined', async () => { + const d = stubDefinition() + await createBuild(d, { outDir, distDir }) + expect(existsSync(resolve(outDir, 'spa-loader.json'))).toBe(false) + }) + + it('removes existing outDir before building', async () => { + await fs.mkdir(outDir, { recursive: true }) + await fs.writeFile(resolve(outDir, 'stale.txt'), 'old') + const d = stubDefinition() + await createBuild(d, { outDir, distDir }) + expect(existsSync(resolve(outDir, 'stale.txt'))).toBe(false) + }) + + it('calls d.setup with build mode context', async () => { + const setup = vi.fn(async () => {}) + const d = stubDefinition({ setup }) + await createBuild(d, { outDir, distDir }) + expect(setup).toHaveBeenCalledOnce() + }) +}) From 49004a2872d6f60ce2fa215969f9e816db247359 Mon Sep 17 00:00:00 2001 From: Jack-sh1 Date: Wed, 13 May 2026 22:14:59 +0800 Subject: [PATCH 2/3] fix(test): use Promise.allSettled with retry opts for afterEach cleanup Ensures both outDir and distDir are cleaned up even if one removal fails, and adds maxRetries/retryDelay for transient fs errors. --- packages/devframe/src/adapters/build.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/devframe/src/adapters/build.test.ts b/packages/devframe/src/adapters/build.test.ts index 837b120..0f9fcb3 100644 --- a/packages/devframe/src/adapters/build.test.ts +++ b/packages/devframe/src/adapters/build.test.ts @@ -26,8 +26,15 @@ describe('createBuild', () => { }) afterEach(async () => { - await fs.rm(outDir, { recursive: true, force: true }) - await fs.rm(distDir, { recursive: true, force: true }) + const rmOpts = { recursive: true, force: true, maxRetries: 3, retryDelay: 100 } as const + await Promise.allSettled([ + fs.rm(outDir, rmOpts).catch((err) => { + console.error(`Failed to cleanup outDir at ${outDir}:`, err) + }), + fs.rm(distDir, rmOpts).catch((err) => { + console.error(`Failed to cleanup distDir at ${distDir}:`, err) + }), + ]) }) it('throws when no distDir is provided', async () => { From 15c015b690e319af93279809f6636dca9cef3dfd Mon Sep 17 00:00:00 2001 From: Jack-sh1 Date: Wed, 13 May 2026 23:11:12 +0800 Subject: [PATCH 3/3] fix(test): warn on cleanup failure with manual deletion hint --- packages/devframe/src/adapters/build.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devframe/src/adapters/build.test.ts b/packages/devframe/src/adapters/build.test.ts index 0f9fcb3..13f438b 100644 --- a/packages/devframe/src/adapters/build.test.ts +++ b/packages/devframe/src/adapters/build.test.ts @@ -29,10 +29,10 @@ describe('createBuild', () => { const rmOpts = { recursive: true, force: true, maxRetries: 3, retryDelay: 100 } as const await Promise.allSettled([ fs.rm(outDir, rmOpts).catch((err) => { - console.error(`Failed to cleanup outDir at ${outDir}:`, err) + console.warn(`Failed to cleanup outDir at ${outDir}, manual deletion required:`, err) }), fs.rm(distDir, rmOpts).catch((err) => { - console.error(`Failed to cleanup distDir at ${distDir}:`, err) + console.warn(`Failed to cleanup distDir at ${distDir}, manual deletion required:`, err) }), ]) })