chore: release 1.0.2-beta12

This commit is contained in:
Disy committed 2026-10-21 09:16:55 +08:00
1 parent bf95d715a4
commit c44fb1a0a5
43 files changed
+2130 -470

No files matched your search

@@ -0,0 +1,27 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { shouldDelayAppUpdate } from './app-update-policy.ts'
const now = Date.parse('2026-10-21T08:00:00Z')
const recent = { publishedAt: '2026-10-21T07:00:00Z' }
test('administrator rollbacks bypass the normal release waiting period', () => {
assert.equal(shouldDelayAppUpdate({ ...recent, rollback: true }, 'release', false, now), false)
assert.equal(shouldDelayAppUpdate({ rollback: true }, 'release', false, now), false)
})
test('ordinary releases retain the 24-hour delay and missing dates remain delayed', () => {
assert.equal(shouldDelayAppUpdate(recent, 'release', false, now), true)
assert.equal(shouldDelayAppUpdate({}, 'release', false, now), true)
assert.equal(
shouldDelayAppUpdate({ publishedAt: '2026-10-20T08:00:00Z' }, 'release', false, now),
false,
)
})
test('beta, forced updates and immediate fetching still bypass the delay', () => {
assert.equal(shouldDelayAppUpdate(recent, 'beta', false, now), false)
assert.equal(shouldDelayAppUpdate({ ...recent, forceUpdate: true }, 'release', false, now), false)
assert.equal(shouldDelayAppUpdate(recent, 'release', true, now), false)
})
@@ -0,0 +1,11 @@
export function shouldDelayAppUpdate(
update: { publishedAt?: string; forceUpdate?: boolean; rollback?: boolean },
channel: string,
immediateUpdateFetch: boolean,
now = Date.now(),
): boolean {
if (channel !== 'release' || immediateUpdateFetch || update.forceUpdate || update.rollback)
return false
const publishedAt = Date.parse(update.publishedAt ?? '')
return !Number.isFinite(publishedAt) || now < publishedAt + 24 * 60 * 60 * 1000
}
@@ -1,20 +1,32 @@
import assert from 'node:assert/strict'
import { registerHooks } from 'node:module'
import test from 'node:test'
import {
receiveSkinSiteMessage,
resetSkinSiteSession,
setSkinSiteFrame,
SKIN_SITE_ORIGIN,
} from '../composables/skin-site-session.ts'
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === '@tauri-apps/plugin-http') {
return {
url: `data:text/javascript,${encodeURIComponent(`export const fetch=async url=>Response.json({payload:url.endsWith('/user/refresh')?{jwt:'fixture.jwt'}:url.endsWith('/user')?{uuid:'owner',username:'Owner',permissions:[]}:[]})`)}`,
shortCircuit: true,
}
}
if (specifier.startsWith('@/')) {
return nextResolve(new URL(`../${specifier.slice(2)}.ts`, import.meta.url).href, context)
}
return nextResolve(specifier, context)
},
})
const { initSkinSiteSession, resetSkinSiteSession, setSkinSiteFrame } =
await import('../composables/skin-site-session.ts')
hooks.deregister()
import { useNetworkStatus } from '../composables/useNetworkStatus.ts'
import {
type InstancePlayer,
const {
onInstancePlayerChanged,
prepareInstancePlayer,
registerInstancePlayerPicker,
saveInstancePlayer,
} from './instance-player.ts'
} = await import('./instance-player.ts')
import type { InstancePlayer } from './instance-player.ts'
test('instance player selection persists independently from the globally selected account', async () => {
const previousNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
@@ -84,23 +96,16 @@ test('signed-in skin site renews the saved player without prompting and only not
}
const calls: string[] = []
let failSave = false
const frame = {
postMessage(data: { type: string; requestId: string }) {
if (data.type === 'starlight-pack-token-request')
receiveSkinSiteMessage(
{
origin: SKIN_SITE_ORIGIN,
source: frame,
data: {
type: 'starlight-pack-token-result',
requestId: data.requestId,
token: 'fixture.jwt',
},
} as unknown as MessageEvent,
frame,
)
const previousStorage = Object.getOwnPropertyDescriptor(globalThis, 'localStorage')
const storage = new Map([['starlight-skin-site-token', 'old.jwt']])
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
value: {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key),
},
} as unknown as Window
})
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
@@ -128,19 +133,7 @@ test('signed-in skin site renews the saved player without prompting and only not
const stop = onInstancePlayerChanged((id) => changes.push(id))
try {
resetSkinSiteSession()
setSkinSiteFrame(frame)
receiveSkinSiteMessage(
{
origin: SKIN_SITE_ORIGIN,
source: frame,
data: {
type: 'starlight-skin-session',
status: 'signed-in',
user: { uuid: 'owner', username: 'Owner' },
},
} as unknown as MessageEvent,
frame,
)
await initSkinSiteSession()
await prepareInstancePlayer('one')
assert.equal(prompts, 0)
assert.ok(calls.includes('plugin:auth|login_skin_site_player'))
@@ -149,6 +142,7 @@ test('signed-in skin site renews the saved player without prompting and only not
failSave = true
await assert.rejects(saveInstancePlayer('two', player), /Save failed/)
assert.deepEqual(changes, ['one'])
storage.clear()
resetSkinSiteSession()
await prepareInstancePlayer('one')
assert.equal(prompts, 1)
@@ -157,6 +151,8 @@ test('signed-in skin site renews the saved player without prompting and only not
unregister()
resetSkinSiteSession()
setSkinSiteFrame(null)
if (previousStorage) Object.defineProperty(globalThis, 'localStorage', previousStorage)
else Reflect.deleteProperty(globalThis, 'localStorage')
if (previous) Object.defineProperty(globalThis, 'window', previous)
else Reflect.deleteProperty(globalThis, 'window')
if (previousNavigator) Object.defineProperty(globalThis, 'navigator', previousNavigator)
@@ -72,7 +72,7 @@ test('notice query uses the target instance value and registered Tauri command',
)
const helperSource = readFileSync(new URL('./instance.ts', import.meta.url), 'utf8')
const tauriSource = readFileSync(
new URL('../../../app/src/api/instance.rs', import.meta.url),
new URL('../../../app/src/api/instance/mod.rs', import.meta.url),
'utf8',
)
assert.match(querySource, /\['post-upgrade-notice', instanceId\]/)
@@ -1,7 +1,23 @@
import assert from 'node:assert/strict'
import { registerHooks } from 'node:module'
import test from 'node:test'
import { loadSkinArmorPreview, saveSkinArmorPreview } from './skin-armor-preview.ts'
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === '@modrinth/ui') {
return nextResolve(
new URL(
'../../../../packages/ui/src/composables/skin-rendering/armor-preview-types.ts',
import.meta.url,
).href,
context,
)
}
return nextResolve(specifier, context)
},
})
const { loadSkinArmorPreview, saveSkinArmorPreview } = await import('./skin-armor-preview.ts')
hooks.deregister()
const originalStorage = Object.getOwnPropertyDescriptor(globalThis, 'localStorage')
@@ -414,8 +414,8 @@ export function skinSitePublicDonationSummary(): Promise<
return skinSiteRequest<SkinSitePublicDonationSummary>('donation/summary', { auth: false })
}
export function skinSiteDonationList(offset = 0): Promise<SkinSiteResponse<SkinSiteDonationPage>> {
return skinSiteRequest<SkinSiteDonationPage>(`donation/list?limit=20&offset=${offset}`, {
export function skinSiteDonationList(offset = 0): Promise<SkinSiteResponse<SkinSitePublicDonationPage>> {
return skinSiteRequest<SkinSitePublicDonationPage>(`donation/list?limit=20&offset=${offset}`, {
auth: false,
})
}
@@ -1214,6 +1214,10 @@ export function skinSiteActivateLauncherRelease(
})
}
export function skinSiteDeleteLauncherRelease(id: number): Promise<SkinSiteResponse<boolean>> {
return skinSiteRequest<boolean>('launcher/admin/' + id, { method: 'DELETE' })
}
export interface SkinSiteLauncherUploadStatus {
id: string
chunkSize: number
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict'
import { registerHooks } from 'node:module'
import test from 'node:test'
const testGlobal = globalThis as unknown as {
skinImageTestFetch: (url: string) => Promise<Response>
}
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === '@tauri-apps/plugin-http') {
return {
url: 'data:text/javascript,export const fetch=(...args)=>globalThis.skinImageTestFetch(...args)',
shortCircuit: true,
}
}
return nextResolve(specifier, context)
},
})
const { fetchImageAsDataUrl } = await import('./skin-site-render.ts')
hooks.deregister()
test.beforeEach((t) => {
t.mock.method(globalThis, 'fetch', async () => {
throw new TypeError('Cross-origin image request blocked')
})
const previousReader = Object.getOwnPropertyDescriptor(globalThis, 'FileReader')
Object.defineProperty(globalThis, 'FileReader', {
configurable: true,
value: class {
result: string | null = null
onload?: () => void
onerror?: () => void
readAsDataURL(blob: Blob) {
void blob.arrayBuffer().then(
(bytes) => {
this.result = `data:${blob.type};base64,${Buffer.from(bytes).toString('base64')}`
this.onload?.()
},
() => this.onerror?.(),
)
}
},
})
t.after(() => {
if (previousReader) Object.defineProperty(globalThis, 'FileReader', previousReader)
else Reflect.deleteProperty(globalThis, 'FileReader')
})
})
test('skin textures download through the native client when browser CORS blocks the site', async () => {
testGlobal.skinImageTestFetch = async () =>
new Response(new Uint8Array([137, 80, 78, 71]), { headers: { 'Content-Type': 'image/png' } })
assert.equal(
await fetchImageAsDataUrl('https://skin.starlight.cool/yggdrasil/textures/test'),
'data:image/png;base64,iVBORw==',
)
})
test('failed image downloads report the HTTP error instead of accepting the response as a texture', async () => {
testGlobal.skinImageTestFetch = async () => new Response('Unavailable', { status: 503 })
await assert.rejects(
fetchImageAsDataUrl('https://skin.starlight.cool/yggdrasil/textures/test'),
/HTTP 503/,
)
})
@@ -1,3 +1,5 @@
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
/**
* Renders a player's skin into a base64 head image, mirroring the skin site's
* own head cropper (front face at 8,8 plus the hat overlay at 40,8).
@@ -28,9 +30,9 @@ function loadImage(src: string): Promise<HTMLImageElement> {
})
}
/** Crop the head (and hat overlay) out of a skin image URL. Returns a data URL. */
export async function renderSkinHead(skinUrl: string, scale = 2): Promise<string> {
const img = await loadImage(skinUrl)
/** Crop the head (and hat overlay) from a locally downloaded skin data URL. */
export async function renderSkinHead(skinDataUrl: string, scale = 2): Promise<string> {
const img = await loadImage(skinDataUrl)
const { canvas, ctx } = getBuffer()
canvas.width = HEAD_BUFFER_SIZE
@@ -74,7 +76,7 @@ export async function renderSkinHead(skinUrl: string, scale = 2): Promise<string
/** Fetch an image URL as a data URL (used to keep full skin textures local). */
export async function fetchImageAsDataUrl(url: string): Promise<string> {
const response = await fetch(url)
const response = await tauriFetch(url)
if (!response.ok) throw new Error(`无法加载图片(HTTP ${response.status})`)
const blob = await response.blob()
return await new Promise<string>((resolve, reject) => {