fix: resolve dev panel and CLI regressions

Synchronize scheduler concurrency across API and gRPC processes and restore persisted limits during recovery.

Serialize subscription mutations and preserve shared storage while cleaning Shell and CLI namespaces, historical references, and filesystem-specific naming candidates.

Handle CLI text responses correctly and include script history in backups. Add regression coverage for concurrency, cleanup ownership, long URLs, Unicode paths, and history exports.
This commit is contained in:
whyour
2026-10-07 12:35:10 +08:00
parent fa8910b3d6
commit 24f7d1a46f
22 changed files with 1595 additions and 57 deletions
@@ -0,0 +1,83 @@
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 { spawn } = require('node:child_process');
const load = require('../helpers/load-security-module.cjs');
function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-subscription-lock-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const config = { scriptPath: path.join(root, 'scripts/') };
const { withSubscriptionMutation } = load(
'back/shared/subscriptionMutationLock.ts',
{ '../config': config },
);
return { root, config, withSubscriptionMutation };
}
test('subscription ownership mutations exclude a second process until cleanup completes', async (t) => {
const f = fixture(t);
let release, entered;
const gate = new Promise((resolve) => (release = resolve));
const held = new Promise((resolve) => (entered = resolve));
t.after(() => release());
const first = f.withSubscriptionMutation(async () => {
entered();
await gate;
});
await held;
const marker = path.join(f.root, 'second-writer');
const source = `
const load = require(${JSON.stringify(
path.resolve('test/helpers/load-security-module.cjs'),
)});
const { withSubscriptionMutation } = load(${JSON.stringify(
path.resolve('back/shared/subscriptionMutationLock.ts'),
)}, { '../config': ${JSON.stringify(f.config)} });
process.send('attempt');
withSubscriptionMutation(async () => require('fs').writeFileSync(${JSON.stringify(
marker,
)}, 'done'))
.then(() => process.exit(0), (error) => { console.error(error); process.exit(1); });
`;
const child = spawn(process.execPath, ['-e', source], {
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
});
t.after(() => child.kill());
let errors = '';
child.stderr.on('data', (chunk) => (errors += chunk));
const completed = new Promise((resolve, reject) => {
child.once('error', reject);
child.once('exit', (code) =>
code === 0
? resolve()
: reject(new Error(errors || `child exit ${code}`)),
);
});
await new Promise((resolve) => child.once('message', resolve));
await new Promise((resolve) => setTimeout(resolve, 150));
assert.equal(fs.existsSync(marker), false);
release();
await Promise.all([first, completed]);
assert.equal(fs.readFileSync(marker, 'utf8'), 'done');
assert.equal(
fs.existsSync(`${path.resolve(f.config.scriptPath)}.subscriptions.lock`),
false,
);
});
test('failed subscription mutations release the lock before the next operation', async (t) => {
const f = fixture(t);
await assert.rejects(
f.withSubscriptionMutation(async () => {
throw new Error('failed cleanup');
}),
/failed cleanup/,
);
assert.equal(
await f.withSubscriptionMutation(async () => 'next mutation'),
'next mutation',
);
});