fix: 修复任务生命周期与调度就绪,优化执行和构建开销 (#3069)

* fix: harden task lifecycle and scheduler readiness

* fix: confine log writes to the configured log directory

* fix: verify complete build artifacts and untracked inputs

* fix: reconcile scheduler state and make stop win startup races

* fix: isolate cron generations and serialize scheduler recovery
This commit is contained in:
whyour
2026-09-13 00:32:41 +08:00
committed by GitHub
parent d62d8f3025
commit a94e665054
61 changed files with 4214 additions and 608 deletions
+184
View File
@@ -0,0 +1,184 @@
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 { spawnSync } = require('node:child_process');
const source = fs
.readFileSync(path.resolve('shell/api.sh'), 'utf8')
.replace(/\nget_token\s*$/, '\n');
const realJq = spawnSync('which', ['jq'], { encoding: 'utf8' }).stdout.trim();
function fixture(t) {
assert.ok(realJq, 'jq is required');
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-api-parse-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const bin = path.join(root, 'bin');
fs.mkdirSync(bin);
fs.writeFileSync(
path.join(bin, 'jq'),
'#!/bin/bash\nprintf "call\\n" >> "$JQ_CALLS"\nexec "$REAL_JQ" "$@"\n',
{ mode: 0o755 },
);
const token = path.join(root, 'token.json'),
response = path.join(root, 'response.json'),
calls = path.join(root, 'calls');
const run = (code) =>
spawnSync(
'/bin/bash',
[
'-euc',
source +
'\ncreate_token(){ __ql_token__=generated; generated=1; }; curl(){ cat "$RESPONSE_FILE"; };\n' +
code,
],
{
encoding: 'utf8',
env: {
...process.env,
PATH: bin + ':' + process.env.PATH,
REAL_JQ: realJq,
JQ_CALLS: calls,
file_auth_token: token,
RESPONSE_FILE: response,
ql_port: '5700',
__ql_token__: 'test-token',
},
},
);
return {
token,
response,
run,
count: () =>
fs.existsSync(calls)
? fs.readFileSync(calls, 'utf8').trim().split('\n').length
: 0,
};
}
test('valid token cache is read with one jq and reused without generation', (t) => {
const f = fixture(t);
fs.writeFileSync(
f.token,
JSON.stringify({
value: 'header.payload.signature',
expiration: 4102444800,
}),
);
const r = f.run(
'get_token; printf "%s:%s" "$__ql_token__" "${generated:-0}"',
);
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, 'header.payload.signature:0');
assert.equal(f.count(), 1);
});
test('expired, corrupt, missing or header-unsafe token cache refreshes under errexit', (t) => {
const f = fixture(t);
for (const content of [
undefined,
'broken',
'{}',
JSON.stringify({ value: 'old', expiration: 1 }),
JSON.stringify({ value: 'old', expiration: '4102444800' }),
JSON.stringify({ value: '', expiration: 4102444800 }),
JSON.stringify({ value: 'bad\ninjected', expiration: 4102444800 }),
JSON.stringify({ value: 'bad\n', expiration: 4102444800 }),
JSON.stringify({ value: 'bad\rinjected', expiration: 4102444800 }),
]) {
if (content === undefined) fs.rmSync(f.token, { force: true });
else fs.writeFileSync(f.token, content);
const r = f.run('get_token; printf "%s:%s" "$__ql_token__" "$generated"');
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, 'generated:1');
}
});
test('success with a message still parses once and stays silent', (t) => {
const f = fixture(t);
fs.writeFileSync(f.response, JSON.stringify({ code: 200, message: 'ok' }));
const r = f.run('update_cron 1 0 123 log 1');
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, '');
assert.equal(f.count(), 1);
});
test('status and statistics errors preserve multiline Unicode messages', (t) => {
const f = fixture(t);
fs.writeFileSync(
f.response,
JSON.stringify({ code: 500, message: '写入失败\n请重试 "原任务"' }),
);
for (const command of [
'update_cron 1 1 123 log 1 2 7',
'record_cron_stat 1 7 2',
]) {
const r = f.run(command);
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, '写入失败\n请重试 "原任务"\n');
}
assert.equal(f.count(), 2);
});
test('non-JSON errors fall back to raw text and absent messages retain legacy null', (t) => {
const f = fixture(t);
fs.writeFileSync(f.response, 'upstream unavailable');
let r = f.run('update_cron 1 0 123 log 1');
assert.equal(r.status, 0);
assert.equal(r.stdout, 'upstream unavailable\n');
fs.writeFileSync(f.response, '{"code":500}');
r = f.run('record_cron_stat 1 7 2');
assert.equal(r.status, 0);
assert.equal(r.stdout, 'null\n');
});
test('statistics without a task id perform no JSON parsing', (t) => {
const f = fixture(t);
const r = f.run('record_cron_stat "" 0 1 || true');
assert.equal(r.status, 0, r.stderr);
assert.equal(f.count(), 0);
});
test('canonical success resets prior errors and matches jq missing-message semantics', (t) => {
const f = fixture(t);
fs.writeFileSync(f.response, '{"code":200}');
const r = f.run(
'code=500; message=old; ql_parse_status_response "$(cat "$RESPONSE_FILE")"; printf "%s|%s" "$code" "$message"; record_cron_stat 1 0 1',
);
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, '200|null');
assert.equal(f.count(), 0);
});
test('noncanonical JSON always uses jq, including whitespace, duplicate keys and embedded success text', (t) => {
const f = fixture(t);
const cases = [
[' {"code":200} ', '200|null'],
['{"code": 200}', '200|null'],
['{"code":200,"message":"完成\\n第二行"}', '200|完成\n第二行'],
['{"code":200,"data":{"code":500}}', '200|null'],
['{"code":"200"}', '200|null'],
['{"code":200,"code":500,"message":"error"}', '500|error'],
['{"code":500,"message":"{\\"code\\":200}"}', '500|{"code":200}'],
['{"code":500}', '500|null'],
];
for (const [body, expected] of cases) {
fs.writeFileSync(f.response, body);
const r = f.run(
'ql_parse_status_response "$(cat "$RESPONSE_FILE")"; printf "%s|%s" "$code" "$message"',
);
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, expected);
}
assert.equal(f.count(), cases.length);
});
test('success-shaped invalid documents retain the raw-response error fallback', (t) => {
const f = fixture(t);
const cases = [
'{"code":200}trailing',
'{"code":200',
'<html>{"code":200}</html>',
];
for (const body of cases) {
fs.writeFileSync(f.response, body);
const r = f.run('update_cron 1 0 123 log 1');
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, body + '\n');
}
assert.equal(f.count(), cases.length);
});