Move trampoline files (boot-hook, watcher, overlay) into trampoline/ subdirectory. Add upstream Luma v2.3.1 source at repo root with our additions: - Page::Ha (4th nav tab) with placeholder empty state - HA tab config schema and default entity mapping (ha-home/) - Nav keys renamed: "home" → "start", new "ha" tab between Apps and Settings - All state.rs/view.rs/main.rs/tests.rs updated for 4-tab navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
5.4 KiB
JavaScript
143 lines
5.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import vm from 'node:vm';
|
|
|
|
const script = await fs.readFile(new URL('../tv/sync-splash.cjs', import.meta.url), 'utf8');
|
|
const app = 'com.ecklf.luma';
|
|
const state = '/var/lib/luma-home';
|
|
|
|
function fixture({ production = false, config, failWrite = false } = {}) {
|
|
const directory = `/media/${production ? 'cryptofs' : 'developer'}/apps/usr/palm/applications/${app}`;
|
|
const files = new Map([
|
|
[
|
|
`${directory}/appinfo.json`,
|
|
Buffer.from(JSON.stringify({ id: app, type: 'native', main: 'luma' })),
|
|
],
|
|
[`${directory}/splash.png`, Buffer.from('dunes')],
|
|
[`${directory}/splash/forest.png`, Buffer.from('forest')],
|
|
[`${directory}/splash/prism.png`, Buffer.from('prism')],
|
|
[`${state}/app/splash.png`, Buffer.from('old Home splash')],
|
|
]);
|
|
if (config) files.set(`${state}/state.json`, Buffer.from(JSON.stringify(config(directory))));
|
|
const writes = [];
|
|
const renames = [];
|
|
const missing = () => Object.assign(new Error('Missing'), { code: 'ENOENT' });
|
|
const mockFs = {
|
|
readFileSync(file, encoding) {
|
|
if (!files.has(file)) throw missing();
|
|
const data = files.get(file);
|
|
return encoding ? data.toString(encoding) : data;
|
|
},
|
|
writeFileSync(file, data, options) {
|
|
assert.ok(file.endsWith('/splash.png.new'));
|
|
assert.equal(options.mode, 0o644);
|
|
if (failWrite) throw Object.assign(new Error('Read-only filesystem'), { code: 'EROFS' });
|
|
writes.push(file);
|
|
files.set(file, data);
|
|
},
|
|
renameSync(from, to) {
|
|
assert.ok(files.has(from));
|
|
renames.push(to);
|
|
files.set(to, files.get(from));
|
|
files.delete(from);
|
|
},
|
|
};
|
|
return {
|
|
directory,
|
|
files,
|
|
writes,
|
|
renames,
|
|
sync(background, overrides = {}) {
|
|
const logs = [];
|
|
vm.runInNewContext(
|
|
script,
|
|
{
|
|
require(name) {
|
|
if (name === 'fs') return mockFs;
|
|
assert.equal(name, 'path', 'the helper must not invoke system or TV commands');
|
|
return path.posix;
|
|
},
|
|
process: { argv: ['node', JSON.stringify({ directory, background, ...overrides })] },
|
|
console: { log: (line) => logs.push(line) },
|
|
},
|
|
{ timeout: 2000 },
|
|
);
|
|
assert.deepEqual(logs.map(JSON.parse), [{ returnValue: true, background }]);
|
|
},
|
|
};
|
|
}
|
|
|
|
test('normal developer and production tile launches use the saved selection', () => {
|
|
for (const production of [false, true]) {
|
|
const mock = fixture({ production });
|
|
mock.sync('forest');
|
|
assert.equal(mock.files.get(`${mock.directory}/splash.png`).toString(), 'forest');
|
|
mock.sync('prism');
|
|
assert.equal(mock.files.get(`${mock.directory}/splash.png`).toString(), 'prism');
|
|
assert.deepEqual(mock.renames, [
|
|
`${mock.directory}/splash.png`,
|
|
`${mock.directory}/splash.png`,
|
|
]);
|
|
assert.ok(!mock.files.has(`${mock.directory}/splash.png.new`));
|
|
assert.equal(mock.files.get(`${state}/app/splash.png`).toString(), 'old Home splash');
|
|
}
|
|
});
|
|
|
|
test('only an enabled Home stage belonging to this installation is refreshed', () => {
|
|
for (const [enabled, matching] of [
|
|
[true, true],
|
|
[false, true],
|
|
[true, false],
|
|
]) {
|
|
const mock = fixture({
|
|
config: (directory) => ({ enabled, appDir: matching ? directory : '/another-installation' }),
|
|
});
|
|
mock.sync('prism');
|
|
assert.equal(mock.files.get(`${mock.directory}/splash.png`).toString(), 'prism');
|
|
assert.equal(
|
|
mock.files.get(`${state}/app/splash.png`).toString(),
|
|
enabled && matching ? 'prism' : 'old Home splash',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('startup repairs a replaced package splash without rewriting an unchanged Home image', () => {
|
|
const mock = fixture({ config: (directory) => ({ enabled: true, appDir: directory }) });
|
|
mock.sync('forest');
|
|
mock.writes.length = 0;
|
|
mock.sync('forest');
|
|
assert.deepEqual(mock.writes, []);
|
|
mock.files.set(`${mock.directory}/splash.png`, Buffer.from('package update default'));
|
|
mock.sync('forest');
|
|
assert.deepEqual(mock.writes, [`${mock.directory}/splash.png.new`]);
|
|
assert.equal(mock.files.get(`${mock.directory}/splash.png`).toString(), 'forest');
|
|
});
|
|
|
|
test('missing sources and failed writes preserve the previous launch splash', () => {
|
|
for (const failWrite of [false, true]) {
|
|
const mock = fixture({ failWrite });
|
|
assert.throws(() => mock.sync(failWrite ? 'forest' : 'missing'), /Missing|Read-only/);
|
|
assert.equal(mock.files.get(`${mock.directory}/splash.png`).toString(), 'dunes');
|
|
assert.equal(mock.files.get(`${state}/app/splash.png`).toString(), 'old Home splash');
|
|
assert.deepEqual(mock.renames, []);
|
|
}
|
|
});
|
|
|
|
test('unrelated installations and invalid selections cannot update a launch splash', () => {
|
|
const mock = fixture();
|
|
for (const background of ['../forest', '', 'forest.png', null]) {
|
|
assert.throws(() => mock.sync(background), /Invalid splash background/);
|
|
}
|
|
for (const directory of ['/etc', `${mock.directory}/..`, `${mock.directory}-other`]) {
|
|
assert.throws(() => mock.sync('forest', { directory }), /Invalid Luma installation/);
|
|
}
|
|
mock.files.set(
|
|
`${mock.directory}/appinfo.json`,
|
|
Buffer.from(JSON.stringify({ id: 'another.app', type: 'native', main: 'luma' })),
|
|
);
|
|
assert.throws(() => mock.sync('forest'), /installed native Luma package/);
|
|
assert.deepEqual(mock.writes, []);
|
|
});
|