import { readFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { DEFAULT_PORT } from '../server/index.mjs' const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') async function readProjectFile(relativePath) { return readFile(path.join(projectRoot, relativePath), 'utf8') } describe('deployment configuration', () => { it('keeps the release version synchronized across project and deployment metadata', async () => { const [packageText, lockText, compose, readme, changelog] = await Promise.all([ readProjectFile('package.json'), readProjectFile('package-lock.json'), readProjectFile('docker-compose.yml'), readProjectFile('README.zh-CN.md'), readProjectFile('CHANGELOG.md'), ]) const packageJson = JSON.parse(packageText) const packageLock = JSON.parse(lockText) const version = packageJson.version expect(version).toMatch(/^\d+\.\d+\.\d+$/) expect(packageLock.version).toBe(version) expect(packageLock.packages[''].version).toBe(version) expect(compose.match(/^\s*image:\s*personal-web-tools:(\S+)$/m)?.[1]).toBe(version) expect(readme).toContain(`当前应用版本:\`${version}\`。`) expect(readme).toContain(`personal-web-tools-${version}-amd64.tar`) expect(changelog.match(/^## (\d+\.\d+\.\d+) - /m)?.[1]).toBe(version) }) it('keeps the default production port synchronized', async () => { const [dockerfile, compose, environment, script, readme] = await Promise.all([ readProjectFile('Dockerfile'), readProjectFile('docker-compose.yml'), readProjectFile('.env.example'), readProjectFile('web-tools.ps1'), readProjectFile('README.zh-CN.md'), ]) expect(DEFAULT_PORT).toBe(9000) expect(dockerfile).toContain('PORT=9000') expect(dockerfile).toContain('EXPOSE 9000') expect(dockerfile).toContain('http://127.0.0.1:9000/healthz') expect(compose).toContain('${WEB_TOOLS_PORT:-9000}:9000') expect(environment.trim()).toBe('WEB_TOOLS_PORT=9000') expect(script).toMatch(/\[int\]\$Port = 9000/) expect(readme).toContain('默认访问地址:`http://服务器地址:9000`') expect(readme).toContain('生产服务默认监听 `0.0.0.0:9000`') }) it('preserves the Windows and container production safeguards', async () => { const [dockerfile, compose, dockerignore, script] = await Promise.all([ readProjectFile('Dockerfile'), readProjectFile('docker-compose.yml'), readProjectFile('.dockerignore'), readProjectFile('web-tools.ps1'), ]) expect(dockerfile).toContain('USER node') expect(compose).toContain('read_only: true') expect(compose).toContain('no-new-privileges:true') expect(compose).toContain('cap_drop:') expect(dockerignore).toContain('node_modules') expect(dockerignore).toContain('data') expect(script).toContain('#Requires -Version 5.1') expect(script).toContain("@('pull', '--ff-only', $Remote, $Branch)") expect(script).toContain("@('ci', '--include=dev')") }) it('offers an interactive Windows action menu without changing explicit automation actions', async () => { const script = await readProjectFile('web-tools.ps1') expect(script).toContain('function Select-InteractiveAction') expect(script).toContain("[2] Update from Git, rebuild, and start") expect(script).toContain("[3] Update from Git and rebuild only") expect(script).toContain("$PSBoundParameters.ContainsKey('Action')") expect(script).toContain("[ValidateSet('start', 'update')]") expect(script).not.toContain("[string]$Action = 'start'") }) it('provides standard entry points for a new AI session', async () => { const [entryReadme, agentGuide, completeReadme] = await Promise.all([ readProjectFile('README.md'), readProjectFile('AGENTS.md'), readProjectFile('README.zh-CN.md'), ]) expect(entryReadme).toContain('[README.zh-CN.md](README.zh-CN.md)') expect(entryReadme).toContain('[AGENTS.md](AGENTS.md)') expect(agentGuide).toContain('## First actions in a new session') expect(agentGuide).toContain('git status --short --branch') expect(completeReadme).toContain('## AI 接手清单') }) it('keeps the management page under the reserved tools namespace', async () => { const router = await readProjectFile('src/router.ts') expect(router).toContain("path: '/tools/setting'") expect(router).not.toContain("path: '/setting'") }) it('uses the UTF-8 / Hex tool ID as the canonical default route', async () => { const [router, catalogText, navigationText, registry] = await Promise.all([ readProjectFile('src/router.ts'), readProjectFile('shared/tool-catalog.json'), readProjectFile('shared/default-navigation.json'), readProjectFile('src/tools/registry.ts'), ]) const catalog = JSON.parse(catalogText) const navigation = JSON.parse(navigationText) expect(catalog.tools.find(({ title }) => title === 'UTF-8 / Hex')?.id).toBe('utf8-hex') expect(navigation.categories.flatMap(({ tools }) => tools)).toContain('utf8-hex') expect(router).toContain("redirect: '/tools/utf8-hex'") expect(router).not.toContain("'/tools/ascii-hex'") expect(registry).toContain("'utf8-hex': defineAsyncComponent(() => import('./Utf8HexTool.vue'))") }) })