// The window must never be blank: the bundle loads under Tauri's frozen Object.prototype (build/freeze-safe.ts), host // data of any age is made complete before render (src/lib/normalize.ts), errors are reported with the home folder as // ~ (src/lib/crash.ts), and the pieces that show an error panel are wired in. import { describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { runInNewContext } from 'node:vm'; import { parseAst } from 'vite'; import { rewrite } from '../build/freeze-safe'; import { DEFAULT_SETTINGS } from '../src/api'; import { ignoredRejection } from '../src/lib/crash'; import { NO_OVERRIDES } from '../src/lib/identity'; import { normalizeAccounts, normalizeProjects, normalizeSettings, normalizeWorkspace } from '../src/lib/normalize'; const ROOT = join(import.meta.dir, '..'); /** Runs `code` (which defines main()) in a fresh realm, with that realm's Object.prototype frozen as Tauri does. */ const run = (code: string, freeze: boolean, strict: boolean): unknown => JSON.parse(runInNewContext(`${freeze ? 'Object.freeze(Object.prototype);' : ''}(function () { ${strict ? "'use strict';" : ''}\n${code}\nreturn JSON.stringify(main()); })()`) as string); describe('freeze-safe', () => { // d3-color's define() and d3-format's FormatSpecifier, as they ship const d3 = ` function main() { function define(constructor, factory, prototype) { constructor.prototype = factory.prototype = prototype; prototype.constructor = constructor; } function Color() {} function color() {} define(Color, color, { copy() { return 1; } }); function Spec() { this.x = 1; } Spec.prototype.toString = function () { return 'spec'; }; return [new Color().constructor === Color, String(new Spec()), Object.keys(Color.prototype).includes('constructor')]; }`; test('the original code throws under a frozen Object.prototype; the rewritten code does not and behaves the same', () => { const fixed = rewrite(d3, c => parseAst(c))!; expect(fixed).not.toBeNull(); const plain = run(d3, false, true); expect(plain).toEqual([true, 'spec', true]); expect(() => run(d3, true, true)).toThrow(/read ?only property/) // JavaScriptCore (WKWebView) and V8 word it differently; expect(run(fixed, true, true)).toEqual(plain); expect(run(fixed, true, false)).toEqual(plain); expect(run(fixed, false, true)).toEqual(plain); }); test('only member assignments of Object.prototype names are touched; nested ones too', () => { expect(rewrite('a.b = 1; a.toStringX = 2; a.toString == 3;', c => parseAst(c))).toBeNull(); const out = rewrite('"use strict"; x.valueOf = y.toString = f;', c => parseAst(c))!; expect(out.startsWith('"use strict";')).toBe(true); expect(out).toContain('__storeboundFreezeSafeSet(x, "valueOf", __storeboundFreezeSafeSet(y, "toString", f))'); }); test('the Vite build uses it', () => { expect(readFileSync(join(ROOT, 'vite.config.ts'), 'utf8')).toMatch(/plugins: \[freezeSafe\(\)/); }); }); describe('host data of any age', () => { test('an empty or broken workspace gives complete defaults', () => { for (const w of [undefined, null, {}, { studios: 'x', overrides: [], settings: 3 }]) { expect(normalizeWorkspace(w)).toEqual({ studios: [], overrides: NO_OVERRIDES, settings: DEFAULT_SETTINGS }); } }); test('partial overrides and settings keep what is there', () => { const w = normalizeWorkspace({ overrides: { ignored: ['project:a', 5] }, settings: { refresh: { autoMinutes: 15 }, channel: 'beta' } }); expect(w.overrides).toEqual({ ...NO_OVERRIDES, ignored: ['project:a'] }); expect(w.settings.refresh).toEqual({ ...DEFAULT_SETTINGS.refresh, autoMinutes: 15 }); expect(w.settings.channel).toBe('beta'); expect(normalizeSettings({ cache: { keepDays: 'x' } }).cache.keepDays).toBe(30); }); test('projects: missing folders flagged, bad entries dropped, names filled', () => { const p = normalizeProjects([ { id: 'a', path: '/gone/demo-game', name: 'Prism Puddle', playAccount: null, appleAccount: null, added: '2026-10-11T00:00:00Z', missing: true }, { id: 'b', path: '/x/Tidy' }, { id: 'a', path: '/dup' }, { path: '/no-id' }, 'junk', ]); expect(p.map(x => [x.id, x.name, x.missing])).toEqual([['a', 'Prism Puddle', true], ['b', 'Tidy', false]]); expect(normalizeProjects(undefined)).toEqual([]); expect(normalizeAccounts([{ id: 'x', kind: 'play' }, { id: 'y', kind: 'fax' }])).toEqual([{ id: 'x', label: 'x', kind: 'play', created: '', studio: null, profile: null }]); // 0.8: the credential profile shared with the CLI; a folder pinning another storebound version expect(normalizeAccounts([{ id: 'z', kind: 'apple', profile: 'default' }])[0]!.profile).toBe('default'); expect(normalizeProjects([{ id: 'p', path: '/w/a', storeboundPin: '0.6.0' }])[0]!.storeboundPin).toBe('0.6.0'); }); }); describe('error screens are wired in', () => { const src = (f: string) => readFileSync(join(ROOT, f), 'utf8'); test('boot.js loads before the bundle and React marks the start', () => { const html = src('index.html'); expect(html.indexOf('/boot.js')).toBeGreaterThan(0); expect(html.indexOf('/boot.js')).toBeLessThan(html.indexOf('/src/main.tsx')); expect(src('public/boot.js')).toContain("'log_webview'"); expect(src('src/main.tsx')).toContain("dataset.booted = '1'"); expect(src('src/main.tsx')).toContain(''); }); test("the notification plugin's refused permission probe is not reported (boot.js and crash.ts)", () => { // seen on Linux in CI (Xvfb): the plugin's init script calls is_permission_granted, which the capability refuses const probe = 'Command plugin:notification|is_permission_granted not allowed by ACL'; expect(ignoredRejection(probe)).toBe(true); expect(ignoredRejection(new Error(probe))).toBe(true); expect(ignoredRejection('Command plugin:notification|notify not allowed by ACL')).toBe(false); expect(ignoredRejection(new Error('sidecar did not start'))).toBe(false); expect(src('public/boot.js')).toContain('/plugin:notification\\|is_permission_granted not allowed/.test(text(e))'); }); test('every screen and app tab has its own boundary', () => { expect(src('src/App.tsx')).toMatch(/