Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/main/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import type { UptimeReport } from '@shared/uptime'
import type { JavaArgsConfig, MetricSeries, ServerConfig, ServerEvent } from '@shared/types'
import { alertsPath, uploadsDir, auditDir } from './paths'
import { analyzeCrash } from './core/crash'
import { CREATABLE_TYPES } from '@shared/versions'
import { CREATABLE_TYPES, createErrorKey } from '@shared/versions'

/* eslint-disable no-console */

Expand Down Expand Up @@ -2713,6 +2713,54 @@ export async function runWizardSmoke(): Promise<void> {
console.log('WIZARD-SMOKE: empty-download guard OK (0-byte body fails clearly, no checksum confusion, dest removed)')
}

// 5. Error legibility (#44): every raw code the creation path can throw must
// map to a non-raw wizard.* message; a genuinely-unknown string must still
// pass through. Pure + deterministic (no network).
{
const codes = [
'no-build',
'no-download',
'no-server-jar-for-version',
'unknown-version',
'no-forge-build',
'no-neoforge-build',
'no-mohist-build',
'empty-download: http://x/y.jar returned 0 bytes',
'Checksum mismatch (got ab…, expected cd…)',
'HTTP 404 for http://x/y.jar',
'installer exited 1: boom',
'installer-args-not-found',
'folder-exists',
'no-provider-for-banana'
]
const unmapped = codes.filter((c) => createErrorKey(c) === null)
if (unmapped.length) return fail('create error codes not mapped to a message: ' + unmapped.join(', '))
if (createErrorKey('some novel unexpected failure') !== null) {
return fail('createErrorKey must pass unknown strings through (null), not swallow them')
}
console.log(`WIZARD-SMOKE: create-error legibility OK (${codes.length} codes mapped, unknown passes through)`)
}

// 6. Provider matrix walk (#44, inspection): resolving a bogus version per
// provider must never yield a MALFORMED descriptor (that would fail
// illegibly). Throws are expected and tolerated — network-dependent — but
// logged with whether the code maps, so gaps surface without flakiness.
{
for (const type of CREATABLE_TYPES) {
try {
const r = await getProvider(type).resolve('0.0.0-nope')
if (!/^https?:\/\/.+/.test(r.url) || !r.fileName) {
return fail(`${type}: bogus version resolved to a malformed descriptor: ${JSON.stringify(r)}`)
}
console.log(`WIZARD-SMOKE: matrix ${type} bogus -> deferred (${r.url.slice(0, 52)}…)`)
} catch (e) {
const msg = String((e as Error)?.message ?? e)
console.log(`WIZARD-SMOKE: matrix ${type} bogus -> throw [${createErrorKey(msg) ? 'mapped' : 'RAW'}] ${msg.slice(0, 64)}`)
}
}
console.log('WIZARD-SMOKE: provider matrix walked (bogus-version legibility inspected)')
}

console.log('WIZARD-SMOKE: PASS')
app.exit(0)
}
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,11 @@ export default {
errNoBuild: 'No build is available for this Minecraft version yet — pick another version.',
errEmptyDownload: 'The download server returned an empty file. That build may be temporarily unavailable — try another build or version.',
errFolderExists: 'A server folder with that name already exists — choose a different name.',
errNoLauncher: 'The installer finished but produced no launchable server. This loader/version combination may be unsupported.'
errNoLauncher: 'The installer finished but produced no launchable server. This loader/version combination may be unsupported.',
errChecksum: 'The downloaded file was corrupted (checksum did not match). Please try again.',
errNetwork: 'Could not download from the server — it may be offline or that file was removed. Check your connection, try again, or pick another version.',
errInstaller: 'The loader installer failed to run. This version may need a different Java version.',
errUnsupportedType: 'This server type is not supported.'
},
players: {
title: 'Players',
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/src/locales/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,11 @@ const tr: typeof en = {
errNoBuild: 'Bu Minecraft sürümü için henüz bir yapı (build) yok — başka bir sürüm seçin.',
errEmptyDownload: 'İndirme sunucusu boş bir dosya döndürdü. O yapı geçici olarak erişilemez olabilir — başka bir yapı veya sürüm deneyin.',
errFolderExists: 'Bu adda bir sunucu klasörü zaten var — farklı bir ad seçin.',
errNoLauncher: 'Yükleyici tamamlandı ama çalıştırılabilir bir sunucu üretmedi. Bu yükleyici/sürüm birleşimi desteklenmiyor olabilir.'
errNoLauncher: 'Yükleyici tamamlandı ama çalıştırılabilir bir sunucu üretmedi. Bu yükleyici/sürüm birleşimi desteklenmiyor olabilir.',
errChecksum: 'İndirilen dosya bozuk (sağlama tutmadı). Lütfen tekrar deneyin.',
errNetwork: 'Sunucudan indirilemedi — çevrimdışı olabilir ya da o dosya kaldırılmış olabilir. Bağlantınızı kontrol edin, tekrar deneyin veya başka bir sürüm seçin.',
errInstaller: 'Yükleyici çalıştırılamadı. Bu sürüm farklı bir Java sürümü gerektirebilir.',
errUnsupportedType: 'Bu sunucu türü desteklenmiyor.'
},
players: {
title: 'Oyuncular',
Expand Down
14 changes: 6 additions & 8 deletions src/renderer/src/views/CreateView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import type { TFunction } from 'i18next'
import { Sparkles, Download, Loader2, ExternalLink, ChevronRight, Check } from 'lucide-react'
import { useStore } from '../store'
import { CREATABLE_TYPES, HAS_BUILDS } from '@shared/versions'
import { CREATABLE_TYPES, HAS_BUILDS, createErrorKey } from '@shared/versions'
import type { McVersion, BuildInfo, CreateProgress } from '@shared/versions'
import type { JavaPreset, ServerType } from '@shared/types'

Expand All @@ -14,16 +14,14 @@ const PRESETS: JavaPreset[] = ['aikars', 'aikars-large', 'basic']
* Turn a raw createServer error code into a human message. The backend surfaces
* short codes (e.g. `no-mohist-build` when the upstream API lists a version but
* has no build for it, or `empty-download` when a mirror serves a 0-byte body);
* shown verbatim these read as gibberish to a non-technical user. Unknown codes
* fall through unchanged.
* shown verbatim these read as gibberish to a non-technical user. The code→key
* table lives in @shared/versions (createErrorKey) so it is testable; unmapped
* codes fall through unchanged.
*/
function friendlyCreateError(error: string | undefined, t: TFunction): string {
const e = error ?? '?'
if (/^no-[a-z]+-build$/.test(e)) return t('wizard.errNoBuild')
if (e.startsWith('empty-download')) return t('wizard.errEmptyDownload')
if (e === 'folder-exists') return t('wizard.errFolderExists')
if (e === 'installer-args-not-found') return t('wizard.errNoLauncher')
return e
const key = createErrorKey(e)
return key ? t(key) : e
}

export function CreateView(): JSX.Element {
Expand Down
28 changes: 28 additions & 0 deletions src/shared/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ export interface CreateProgress {
message?: string
}

/**
* Map a raw createServer error to an i18n key under `wizard.*`, or null when it
* should be shown verbatim. Pure + centralised so both the wizard UI and the
* smoke rely on the same table — the UI must never surface a bare code like
* `no-mohist-build`. Codes originate in core/versions.ts, core/createServer.ts
* and core/net.ts; keep this in sync when a new `throw new Error(...)` is added.
*/
export function createErrorKey(error: string): string | null {
const e = error.trim()
if (
e === 'no-build' ||
e === 'no-download' ||
e === 'no-server-jar-for-version' ||
e === 'unknown-version' ||
/^no-[a-z]+-build$/.test(e) // no-forge-build, no-neoforge-build, no-mohist-build
) {
return 'wizard.errNoBuild'
}
if (e.startsWith('empty-download')) return 'wizard.errEmptyDownload'
if (e.startsWith('Checksum mismatch')) return 'wizard.errChecksum'
if (e.startsWith('HTTP ')) return 'wizard.errNetwork'
if (e.startsWith('installer exited')) return 'wizard.errInstaller'
if (e === 'installer-args-not-found') return 'wizard.errNoLauncher'
if (e === 'folder-exists') return 'wizard.errFolderExists'
if (e.startsWith('no-provider-for-')) return 'wizard.errUnsupportedType'
return null
}

/** Server types the creation wizard can produce. */
export const CREATABLE_TYPES: ServerType[] = [
'vanilla',
Expand Down
Loading