mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 03:15:13 +08:00
🚀 Sync 2026-09-28 12:35:46
This commit is contained in:
1 parent
8b304e4854
commit
86e1d0663a
155 files changed
-172498
No files matched your search
@@ -1,192 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Frontend protocol inventory.
|
||||
*
|
||||
* homeproxy.js carries one ordered `protocols` table and both forms render
|
||||
* their type picker from it. Before that table existed each form had its own
|
||||
* hand-written value list, and they had drifted in ways nothing could catch:
|
||||
* `snell` had a complete form block, a CREDENTIALS row and a golden outbound
|
||||
* but was not in the node form's list, so it could not be selected at all; the
|
||||
* node form offered `chacha20`, which sing-box rejects outright.
|
||||
*
|
||||
* The form snapshots cannot catch this class of bug - they record an option's
|
||||
* dependency and default, and the node form's picker lives in a subsection the
|
||||
* dump does not reach - so this test reads the table directly and compares it
|
||||
* with the backend tables that decide what the generators can actually build.
|
||||
*
|
||||
* Checks:
|
||||
* 1. every offered type is modelled by the backend (parser/mapping.uc
|
||||
* PROTOCOL_TO_UCI) - the table that makes a protocol reachable from UCI;
|
||||
* 2. every protocol the outbound adapter can build (OPTION_FIELDS,
|
||||
* REQUIRED_CREDENTIALS) is offered by the CLIENT form - "the backend
|
||||
* supports it and the UI cannot select it" is the snell bug;
|
||||
* 3. every protocol offered by the SERVER form has inbound credentials
|
||||
* (model.uc INBOUND_CREDENTIALS) - the reverse direction;
|
||||
* 4. the client and server lists match their expected order, because the
|
||||
* order is part of the UI and filtering one table has to reproduce it;
|
||||
* 5. protocols the backend models but the UI deliberately does not offer are
|
||||
* listed explicitly, so an accidental omission has to be added here (and
|
||||
* this file is reviewed) rather than passing silently.
|
||||
*
|
||||
* Usage: node tests/frontend-protocol-inventory.js <repo-root>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { loadLuciModule } = require('./lib/luci-module.js');
|
||||
|
||||
const root = path.resolve(process.argv[2] || '.');
|
||||
const scripts = path.join(root, 'root/etc/homeproxy/scripts');
|
||||
|
||||
/* The registry is data plus the renderer; loading it through the whole LuCI
|
||||
* mock would test the mock. `protocols` only needs the module to evaluate its
|
||||
* top-level statements, so the imports are stubbed just far enough for that. */
|
||||
function loadHomeproxy() {
|
||||
return loadLuciModule(
|
||||
path.join(root, 'htdocs/luci-static/resources/homeproxy.js'),
|
||||
{
|
||||
baseclass: { extend: (o) => o },
|
||||
form: { DynamicList: { extend: (o) => o } },
|
||||
fs: {}, rpc: {}, uci: {}, ui: {}
|
||||
});
|
||||
}
|
||||
|
||||
/* Read the keys of one `export const NAME = { ... }` table. Only depth-1 keys
|
||||
* count, so nested per-option maps and the `};` terminator are ignored. */
|
||||
function tableKeys(file, name) {
|
||||
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
||||
const head = new RegExp('^export const ' + name + '\\s*=\\s*\\{');
|
||||
let start = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (head.test(lines[i])) { start = i; break; }
|
||||
}
|
||||
if (start === -1)
|
||||
throw new Error(`table ${name} not found in ${file}`);
|
||||
|
||||
const keys = [];
|
||||
let depth = 1;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (depth === 1) {
|
||||
const m = /^\t([A-Za-z0-9_]+)\s*:/.exec(line);
|
||||
if (m) keys.push(m[1]);
|
||||
}
|
||||
for (const ch of line) {
|
||||
if (ch === '{') depth++;
|
||||
else if (ch === '}') depth--;
|
||||
}
|
||||
if (depth <= 0) break;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
let checks = 0, failures = 0;
|
||||
function check(what, ok, detail) {
|
||||
checks++;
|
||||
if (ok) return;
|
||||
failures++;
|
||||
console.error(`FAIL ${what}${detail ? ': ' + detail : ''}`);
|
||||
}
|
||||
|
||||
const hp = loadHomeproxy();
|
||||
const protocols = hp.protocols;
|
||||
check('homeproxy.js exports a protocols table', Array.isArray(protocols) && protocols.length > 0);
|
||||
if (!Array.isArray(protocols)) {
|
||||
console.error(`\n${checks} checks, ${failures} failures`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mapped = tableKeys(path.join(scripts, 'parser/mapping.uc'), 'PROTOCOL_TO_UCI');
|
||||
const optionFields = tableKeys(path.join(scripts, 'config/adapter.uc'), 'OPTION_FIELDS');
|
||||
const required = tableKeys(path.join(scripts, 'config/adapter.uc'), 'REQUIRED_CREDENTIALS');
|
||||
const inboundCredentials = tableKeys(path.join(scripts, 'config/model.uc'), 'INBOUND_CREDENTIALS');
|
||||
const credentials = tableKeys(path.join(scripts, 'config/model.uc'), 'CREDENTIALS');
|
||||
|
||||
const clientList = protocols.filter((p) => p.sides.includes('client')).map((p) => p.type);
|
||||
const serverList = protocols.filter((p) => p.sides.includes('server')).map((p) => p.type);
|
||||
|
||||
check('mapping.uc PROTOCOL_TO_UCI parsed', mapped.length > 0, `${mapped.length} keys`);
|
||||
check('adapter.uc OPTION_FIELDS parsed', optionFields.length > 0, `${optionFields.length} keys`);
|
||||
check('adapter.uc REQUIRED_CREDENTIALS parsed', required.length > 0, `${required.length} keys`);
|
||||
check('model.uc INBOUND_CREDENTIALS parsed', inboundCredentials.length > 0, `${inboundCredentials.length} keys`);
|
||||
|
||||
/* 1. Each side is checked against the table that makes it reachable there.
|
||||
* The client side is the outbound half: PROTOCOL_TO_UCI is what the Loader
|
||||
* reads options through. The server side is the inbound half, which lives
|
||||
* in model.uc's INBOUND_CREDENTIALS - server-only protocols such as naive
|
||||
* and mixed legitimately have no outbound mapping at all. */
|
||||
for (const p of protocols) {
|
||||
check(`'${p.type}' declares at least one side`, p.sides.length > 0);
|
||||
/* A label is now wrapped in _() at the table, so it is a translated
|
||||
* String object rather than a primitive when a translation exists. The
|
||||
* property under test is that it is present and non-empty. */
|
||||
check(`'${p.type}' has a label`, p.label != null && String(p.label).length > 0);
|
||||
|
||||
if (p.sides.includes('client'))
|
||||
check(`client '${p.type}' is modelled by the backend (PROTOCOL_TO_UCI)`, mapped.includes(p.type));
|
||||
|
||||
if (p.sides.includes('server'))
|
||||
check(`server '${p.type}' has inbound credentials (INBOUND_CREDENTIALS)`, inboundCredentials.includes(p.type));
|
||||
}
|
||||
|
||||
/* 2. Outbound-capable in the backend => selectable in the client form. This
|
||||
* is the direction that catches "the backend supports it and the UI cannot
|
||||
* select it", which is how snell was lost. */
|
||||
const outboundCapable = [...new Set([...optionFields, ...required])].sort();
|
||||
for (const type of outboundCapable) {
|
||||
check(`outbound-capable '${type}' can be selected in the node form`, clientList.includes(type));
|
||||
}
|
||||
|
||||
/* 4. The rendered order is part of the UI. Pinned here because the form
|
||||
* snapshots do not reach the node form's picker. */
|
||||
const expectedClient = [
|
||||
'direct', 'anytls', 'http', 'hysteria', 'hysteria2',
|
||||
'shadowsocks', 'shadowtls', 'snell', 'socks', 'ssh',
|
||||
'trojan', 'tuic', 'wireguard', 'vless', 'vmess'
|
||||
];
|
||||
const expectedServer = [
|
||||
'anytls', 'http', 'hysteria', 'hysteria2', 'naive', 'mixed',
|
||||
'shadowsocks', 'snell', 'socks', 'trojan', 'tuic', 'vless', 'vmess'
|
||||
];
|
||||
check('client protocol order', JSON.stringify(clientList) === JSON.stringify(expectedClient),
|
||||
`got ${JSON.stringify(clientList)}`);
|
||||
check('server protocol order', JSON.stringify(serverList) === JSON.stringify(expectedServer),
|
||||
`got ${JSON.stringify(serverList)}`);
|
||||
|
||||
/* 5. Deliberate omissions, spelled out. A protocol the backend models but no
|
||||
* form offers has to appear here; adding one to this list is a reviewable
|
||||
* decision instead of a silent gap. */
|
||||
const DELIBERATE = {
|
||||
/* The backend models the inbound side (INBOUND_CREDENTIALS,
|
||||
* REQUIRED_INBOUND_CREDENTIALS, INBOUND_CLAIM_FIELDS) but the inbound
|
||||
* generation path has no fixture/golden coverage, so the server form must
|
||||
* not offer it until that exists. The client form does offer it. */
|
||||
shadowtls: 'server side has no inbound fixture/golden coverage yet',
|
||||
/* Endpoint protocols: modelled and buildable, but the client form gates
|
||||
* them behind runtime features (wireguard on with_wireguard+gvisor,
|
||||
* direct is always offered). Listed here because they are not part of
|
||||
* the adapter's per-protocol OPTION_FIELDS tables. */
|
||||
wireguard: 'endpoint, gated on with_wireguard + with_gvisor',
|
||||
direct: 'endpoint, always offered by the client form'
|
||||
};
|
||||
|
||||
const unmapped = [...new Set([...mapped, ...credentials, ...inboundCredentials])]
|
||||
.filter((t) => !clientList.includes(t) && !serverList.includes(t))
|
||||
.sort();
|
||||
for (const type of unmapped) {
|
||||
check(`unoffered protocol '${type}' is a documented decision`, type in DELIBERATE,
|
||||
'models it in the backend but no form offers it');
|
||||
}
|
||||
|
||||
console.log(`frontend protocol inventory: ${checks} checks, ${failures} failures`);
|
||||
console.log(` client: ${clientList.join(' ')}`);
|
||||
console.log(` server: ${serverList.join(' ')}`);
|
||||
if (unmapped.length)
|
||||
console.log(` deliberately unoffered: ${unmapped.map((t) => `${t} (${DELIBERATE[t] || 'UNDOCUMENTED'})`).join(', ')}`);
|
||||
|
||||
process.exit(failures ? 1 : 0);
|
||||
Reference in new issue
Block a user