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>
174 lines
5.7 KiB
JavaScript
174 lines
5.7 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/read-app-icon.cjs', import.meta.url), 'utf8');
|
|
const roots = [
|
|
'/media/developer/apps/usr/palm/applications',
|
|
'/media/cryptofs/apps/usr/palm/applications',
|
|
'/usr/palm/applications',
|
|
];
|
|
const directory = `${roots[0]}/sample`;
|
|
|
|
function readIcon({ metadata = {}, installations = [], chunk = 16384 } = {}) {
|
|
const files = new Map();
|
|
for (const install of installations) {
|
|
const dir = install.directory ?? directory;
|
|
if (install.manifest !== undefined) {
|
|
files.set(`${dir}/appinfo.json`, Buffer.from(JSON.stringify(install.manifest)));
|
|
}
|
|
for (const [name, bytes] of Object.entries(install.icons ?? {})) {
|
|
files.set(path.posix.resolve(dir, name), Buffer.from(bytes));
|
|
}
|
|
}
|
|
const opened = [];
|
|
const descriptors = new Set();
|
|
const logs = [];
|
|
const errors = [];
|
|
const mockFs = {
|
|
openSync(file) {
|
|
opened.push(file);
|
|
if (!files.has(file)) throw Object.assign(new Error('Missing'), { code: 'ENOENT' });
|
|
descriptors.add(file);
|
|
return file;
|
|
},
|
|
fstatSync(file) {
|
|
return { size: files.get(file).length, isFile: () => true };
|
|
},
|
|
readSync(file, buffer, offset, length, position) {
|
|
return files.get(file).copy(buffer, offset, position, position + Math.min(length, chunk));
|
|
},
|
|
closeSync(file) {
|
|
assert.ok(descriptors.delete(file));
|
|
},
|
|
};
|
|
vm.runInNewContext(
|
|
script,
|
|
{
|
|
require(name) {
|
|
if (name === 'fs') return mockFs;
|
|
assert.equal(name, 'path');
|
|
return path.posix;
|
|
},
|
|
process: { argv: ['node', JSON.stringify({ id: 'sample', ...metadata })] },
|
|
Buffer,
|
|
console: { log: (line) => logs.push(line), error: (line) => errors.push(line) },
|
|
},
|
|
{ timeout: 2000 },
|
|
);
|
|
assert.equal(descriptors.size, 0, 'all opened files must be closed');
|
|
assert.equal(logs.length, 1);
|
|
const reply = JSON.parse(logs[0]);
|
|
assert.equal(reply.returnValue, true);
|
|
return { icons: reply.icons.map((hex) => Buffer.from(hex, 'hex')), opened, errors };
|
|
}
|
|
|
|
test('runtime helper discovers manifest artwork without any bundled icon map', () => {
|
|
const { icons } = readIcon({
|
|
installations: [
|
|
{
|
|
manifest: { id: 'sample', largeIcon: 'resources/hu/large.png', icon: 'regular.png' },
|
|
icons: { 'resources/hu/large.png': 'large', 'regular.png': 'regular' },
|
|
},
|
|
],
|
|
chunk: 3,
|
|
});
|
|
assert.deepEqual(
|
|
icons.map((bytes) => bytes.toString()),
|
|
['large', 'regular'],
|
|
);
|
|
});
|
|
|
|
test('live inventory installation and localized icon take precedence over other copies', () => {
|
|
const installed = `${roots[1]}/actual-directory`;
|
|
const { icons } = readIcon({
|
|
metadata: { directory: installed, large_icon: `file://${installed}/localized.webp` },
|
|
installations: [
|
|
{ icons: { 'icon.png': 'old developer install' } },
|
|
{ directory: installed, icons: { 'localized.webp': 'current installed artwork' } },
|
|
],
|
|
});
|
|
assert.equal(icons[0].toString(), 'current installed artwork');
|
|
});
|
|
|
|
test('missing, unsupported and oversized large icons fall back to regular or default artwork', () => {
|
|
for (const [name, bytes] of [
|
|
['missing.png', null],
|
|
['large.svg', '<svg/>'],
|
|
['large.png', Buffer.alloc(512 * 1024 + 1)],
|
|
]) {
|
|
const { icons } = readIcon({
|
|
installations: [
|
|
{
|
|
manifest: { id: 'sample', largeIcon: name, icon: 'regular.jpeg' },
|
|
icons: { ...(bytes ? { [name]: bytes } : {}), 'regular.jpeg': 'regular' },
|
|
},
|
|
],
|
|
});
|
|
assert.deepEqual(
|
|
icons.map((data) => data.toString()),
|
|
['regular'],
|
|
);
|
|
}
|
|
const { icons } = readIcon({ installations: [{ icons: { 'icon.png': 'default' } }] });
|
|
assert.equal(icons[0].toString(), 'default');
|
|
});
|
|
|
|
test('artwork responses have an aggregate byte limit and do not duplicate the same path', () => {
|
|
const { icons } = readIcon({
|
|
metadata: { large_icon: 'large.png', icon: 'regular.png' },
|
|
installations: [
|
|
{
|
|
manifest: { id: 'sample', largeIcon: 'large.png', icon: 'regular.png' },
|
|
icons: {
|
|
'large.png': Buffer.alloc(400 * 1024),
|
|
'regular.png': Buffer.alloc(200 * 1024),
|
|
'icon.png': 'default',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
assert.deepEqual(
|
|
icons.map((data) => data.length),
|
|
[400 * 1024, 7],
|
|
);
|
|
});
|
|
|
|
test('invalid paths and unrelated manifests cannot select artwork outside app directories', () => {
|
|
for (const icon of [
|
|
'/etc/passwd.png',
|
|
'../../secret.png',
|
|
'https://example.com/icon.png',
|
|
'bad\0.png',
|
|
]) {
|
|
const { icons, opened } = readIcon({
|
|
metadata: { directory: '/etc', large_icon: icon },
|
|
installations: [
|
|
{
|
|
manifest: { id: 'another-app', largeIcon: 'wrong.png' },
|
|
icons: { 'wrong.png': 'wrong' },
|
|
},
|
|
],
|
|
});
|
|
assert.deepEqual(icons, []);
|
|
assert.ok(opened.every((file) => roots.some((root) => file.startsWith(`${root}/`))));
|
|
assert.ok(!opened.some((file) => file.endsWith('/wrong.png')));
|
|
}
|
|
assert.throws(() => readIcon({ metadata: { id: 'sample; command' } }), /Invalid application ID/);
|
|
const { icons } = readIcon({
|
|
installations: [{ manifest: null, icons: { 'icon.png': 'default' } }],
|
|
});
|
|
assert.equal(icons[0].toString(), 'default');
|
|
});
|
|
|
|
test('file names containing shell punctuation are treated as literal local paths', () => {
|
|
const name = "it's; $(not-a-command).png";
|
|
const { icons } = readIcon({
|
|
metadata: { icon: name },
|
|
installations: [{ icons: { [name]: 'literal file' } }],
|
|
});
|
|
assert.equal(icons[0].toString(), 'literal file');
|
|
});
|