fix: preserve Python source state and repair dev regressions

This commit is contained in:
whyour
2026-10-15 09:53:22 +08:00
parent 13c0a4d21b
commit 53e1129a12
7 changed files with 207 additions and 15 deletions
@@ -61,6 +61,7 @@ test('dependency listing marks cache entries missing from disk for reinstall', a
fileExist: async () => false,
getGetCommand: (_type, name) => `check:${name}`,
getInstallCommand: () => '',
isPythonSourceDependency: () => false,
getPid: async () => 0,
getUninstallCommand: () => '',
killTask: async () => {},
+153
View File
@@ -0,0 +1,153 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const load = require('../helpers/load-security-module.cjs');
const types = { nodejs: 0, python3: 1, linux: 2 };
const statuses = { installed: 1, installFailed: 2, queued: 6, cancelled: 7 };
const util = load('back/config/util.ts', {
'./index': {},
'./const': {},
'../loaders/logger': {},
'../shared/utils': {},
'../data/dependence': { DependenceTypes: types },
});
function serviceFor(docs) {
const probes = [],
removed = [],
uninstalls = [];
const Service = load('back/services/dependence.ts', {
'../config': {},
'../data/dependence': {
Dependence: class {},
DependenceTypes: types,
DependenceStatus: statuses,
versionDependenceCommandTypes: { 0: '@', 1: '==', 2: '=' },
DependenceModel: {
findAll: async ({ where }) =>
docs.filter(
(d) =>
(where.status === undefined || d.status === where.status) &&
(!where.id || where.id.includes(d.id)),
),
update: async (values, { where }) => {
docs
.filter((d) => where.id.includes(d.id))
.forEach((d) => Object.assign(d, values));
},
destroy: async ({ where }) => removed.push(...where.id),
},
},
'../config/util': {
...util,
concurrentRun: (tasks) => Promise.all(tasks.map((fn) => fn())),
getGetCommand: (type, name) => {
probes.push(name);
return util.getGetCommand(type, name);
},
promiseExecSuccess: async () => '',
},
'../shared/pLimit': { removeQueuedDependency() {} },
'./sock': class {},
'../shared/i18n': {},
}).default;
const service = new Service({}, {});
service.installDependenceOneByOne = (dependencies, install, force) => {
uninstalls.push({ ids: dependencies.map((d) => d.id), install, force });
return Promise.resolve();
};
return { service, probes, removed, uninstalls };
}
test('listing preserves Python source results and still detects missing named packages', async () => {
const docs = [
' requirements with spaces/requirements.txt ',
'requirements-dev.in',
'project/pyproject.toml',
'git+https://example.invalid/repo.git',
'https://example.invalid/pkg.whl',
'missing-package',
].map((name, i) => ({ id: i + 1, name, type: 1, status: 1 }));
const { service, probes } = serviceFor(docs);
for (let i = 0; i < 2; i++) {
await service.dependencies({
searchValue: '',
type: 'python3',
status: '',
});
}
assert.deepEqual(
docs.map((d) => d.status),
[1, 1, 1, 1, 1, 2],
);
assert.deepEqual(probes, ['missing-package']);
// Source installs must continue to execute pip rather than treating the source as a package name.
assert.equal(util.getGetCommand(1, docs[0].name), 'echo ""');
});
test('deletion cleans previously misclassified Python sources and only queues intended records', async () => {
const docs = [
{ id: 1, name: 'requirements.txt', type: 1, status: 2 },
{ id: 2, name: 'present-package', type: 0, status: 1 },
{ id: 3, name: 'missing-package', type: 0, status: 2 },
{ id: 4, name: 'queued/requirements.txt', type: 1, status: 6 },
{ id: 5, name: 'cancelled/requirements.txt', type: 1, status: 7 },
{ id: 6, name: 'git+https://example.invalid/repo.git', type: 1, status: 2 },
];
const { service, removed, uninstalls } = serviceFor(docs);
await service.remove(docs.map((d) => d.id));
assert.deepEqual(removed, [3, 4, 5]);
assert.deepEqual(uninstalls, [
{ ids: [1, 2, 6], install: false, force: false },
]);
assert.equal(docs[2].status, 2);
});
test('Alpine container mirror command runs without sudo and writes valid repositories', async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-alpine-mirror-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const source =
'https://dl-cdn.alpinelinux.org/alpine/v3.23/main\nhttps://dl-cdn.alpinelinux.org/alpine/v3.23/community\n';
const api = load('back/config/util.ts', {
'fs/promises': { readFile: async () => source },
'./index': {},
'./const': {},
'./share': {},
'./container': { isInContainer: () => true },
'../loaders/logger': {},
'../shared/utils': {},
'../shared/fileAccess': {},
'../data/dependence': {},
});
const previous = process.env.QL_OS_TYPE;
let command;
try {
process.env.QL_OS_TYPE = 'alpine';
command = await api.updateLinuxMirrorFile('https://mirror.example/alpine');
} finally {
if (previous === undefined) delete process.env.QL_OS_TYPE;
else process.env.QL_OS_TYPE = previous;
}
// Redirect the generated command's paths and executable into the fixture only.
const file = path.join(root, 'repositories');
const update = path.join(root, 'updated');
const sandbox = command
.replaceAll('/etc/apk/repositories', file)
.replaceAll('/etc/apk', root);
execFileSync('/bin/bash', [
'-c',
`sudo(){ return 127; }; apk(){ test "$1" = update && touch '${update}'; }; ${sandbox}`,
]);
assert.ok(fs.existsSync(update));
assert.equal(
fs.readFileSync(file, 'utf8'),
source.replaceAll(
'https://dl-cdn.alpinelinux.org/alpine',
'https://mirror.example/alpine',
),
);
});
+20
View File
@@ -0,0 +1,20 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
test('notification log column titles resolve in every supported locale', () => {
const source = fs.readFileSync(
path.join(__dirname, '../../src/pages/setting/notifyLog.tsx'),
'utf8',
);
const titles = [...source.matchAll(/title:\s*intl\.get\('([^']+)'\)/g)].map(
(m) => m[1],
);
assert.ok(titles.includes('标题') && titles.includes('内容'));
for (const locale of ['zh-CN', 'en-US']) {
const messages = require(`../../src/locales/${locale}.json`);
for (const title of titles)
assert.ok(messages[title]?.trim(), `${locale}: ${title}`);
}
});