mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-29 01:56:37 +08:00
571 lines
19 KiB
JavaScript
571 lines
19 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const { execFileSync, spawnSync } = require('node:child_process');
|
|
const { createContext, sourceEnvironment } = require('../../dist/internal/runtime/context');
|
|
const { executeTask } = require('../../dist/internal/execution/taskRunner');
|
|
|
|
test('Shell and CLI agree on isolated script state and account modes', async (t) => {
|
|
const root = await fs.realpath(
|
|
await fs.mkdtemp(path.join(os.tmpdir(), 'ql-differential-')),
|
|
);
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
const bin = path.join(root, 'bin');
|
|
await fs.mkdir(bin);
|
|
await fs.symlink(process.execPath, path.join(bin, 'node'));
|
|
await fs.writeFile(path.join(bin, 'pnpm'), '#!/bin/sh\nexit 0\n', {
|
|
mode: 0o755,
|
|
});
|
|
const context = createContext(
|
|
{ root },
|
|
{ PATH: `${bin}:/usr/local/bin:/usr/bin:/bin`, no_tee: 'true' },
|
|
);
|
|
for (const directory of [
|
|
context.paths.dir_shell,
|
|
context.paths.dir_preload,
|
|
context.paths.dir_config,
|
|
context.paths.dir_scripts,
|
|
context.paths.dir_log,
|
|
path.join(root, 'static/build'),
|
|
])
|
|
await fs.mkdir(directory, { recursive: true });
|
|
for (const name of ['task.sh', 'otask.sh', 'share.sh', 'api.sh', 'env.sh'])
|
|
await fs.copyFile(
|
|
path.resolve(__dirname, '../../../shell', name),
|
|
path.join(context.paths.dir_shell, name),
|
|
);
|
|
await fs.mkdir(path.join(context.paths.dir_shell, 'lang'));
|
|
for (const name of ['zh.sh', 'en.sh'])
|
|
await fs.copyFile(
|
|
path.resolve(__dirname, '../../../shell/lang', name),
|
|
path.join(context.paths.dir_shell, 'lang', name),
|
|
);
|
|
await fs.writeFile(
|
|
path.join(root, 'static/build/token.js'),
|
|
'process.stdout.write("fixture-local-token")',
|
|
);
|
|
await fs.writeFile(
|
|
context.paths.file_config_user,
|
|
'no_tee=true\nconfiguration_function() { printf "function"; }\n',
|
|
);
|
|
await fs.writeFile(context.paths.file_env, 'export ACCOUNTS="one&two"\n');
|
|
await fs.writeFile(context.paths.list_crontab_user, '');
|
|
await fs.writeFile(
|
|
context.paths.file_task_before,
|
|
'state=before\nprintf "before\\n" >> "$TRACE"\n',
|
|
);
|
|
await fs.writeFile(
|
|
context.paths.file_task_after,
|
|
'printf "after:%s:%s\\n" "$state" "$_task_exit_code" >> "$TRACE"\n',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(context.paths.dir_scripts, 'fixture.sh'),
|
|
'printf "run:%s:%s:%s\\n" "$ACCOUNTS" "$state" "$(configuration_function)" >> "$TRACE"\nstate=script\n',
|
|
);
|
|
for (const mode of ['now', 'desi', 'conc'])
|
|
await t.test(mode, async () => {
|
|
const legacy = path.join(root, `${mode}-legacy`),
|
|
modern = path.join(root, `${mode}-modern`);
|
|
const args =
|
|
mode === 'now'
|
|
? ['fixture.sh', 'now']
|
|
: ['fixture.sh', mode, 'ACCOUNTS', '2', '1'];
|
|
execFileSync(
|
|
'/bin/bash',
|
|
[path.join(context.paths.dir_shell, 'task.sh'), ...args],
|
|
{
|
|
cwd: root,
|
|
env: { ...context.env, TRACE: legacy },
|
|
timeout: 15000,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
},
|
|
);
|
|
const env = await sourceEnvironment({ ...context.env, TRACE: modern }, [
|
|
context.paths.file_config_user,
|
|
]);
|
|
const result = await executeTask(
|
|
{ ...context, env },
|
|
{
|
|
argv: ['fixture.sh'],
|
|
mode,
|
|
variable: mode === 'now' ? undefined : 'ACCOUNTS',
|
|
selection: mode === 'now' ? undefined : '2 1',
|
|
},
|
|
);
|
|
assert.equal(result.exitCode, 0);
|
|
const oldLines = (await fs.readFile(legacy, 'utf8')).trim().split('\n');
|
|
const newLines = (await fs.readFile(modern, 'utf8')).trim().split('\n');
|
|
assert.equal(oldLines[0], 'before');
|
|
assert.equal(newLines[0], 'before');
|
|
assert.equal(newLines.at(-1), oldLines.at(-1));
|
|
assert.deepEqual(
|
|
newLines.slice(1, -1).sort(),
|
|
oldLines.slice(1, -1).sort(),
|
|
);
|
|
});
|
|
await t.test(
|
|
'Shell failure, explicit exit and EXIT traps preserve hook behavior',
|
|
async () => {
|
|
const cases = [
|
|
{ name: 'return', script: 'return 7', code: 7, legacy: 0, after: true },
|
|
{ name: 'exit', script: 'exit 7', code: 7, legacy: 0, after: true },
|
|
{
|
|
name: 'errexit',
|
|
script: 'set -e; false',
|
|
code: 1,
|
|
legacy: 0,
|
|
after: true,
|
|
},
|
|
{
|
|
name: 'trap-return',
|
|
script: 'trap \'printf "exit-trap\\n" >> "$TRACE"\' EXIT; return 7',
|
|
code: 7,
|
|
legacy: 0,
|
|
after: true,
|
|
trap: true,
|
|
},
|
|
{
|
|
name: 'trap-exit',
|
|
script: 'trap \'printf "exit-trap\\n" >> "$TRACE"\' EXIT; exit 7',
|
|
code: 7,
|
|
legacy: 0,
|
|
after: true,
|
|
trap: true,
|
|
},
|
|
];
|
|
await fs.writeFile(context.paths.file_config_user, 'no_tee=true\n');
|
|
await fs.writeFile(
|
|
context.paths.file_task_before,
|
|
'printf "before\\n" >> "$TRACE"\n',
|
|
);
|
|
await fs.writeFile(
|
|
context.paths.file_task_after,
|
|
'printf "after:%s\\n" "$_task_exit_code" >> "$TRACE"\n',
|
|
);
|
|
for (const item of cases) {
|
|
await fs.writeFile(
|
|
path.join(context.paths.dir_scripts, 'failure.sh'),
|
|
item.script + '\n',
|
|
);
|
|
const legacyTrace = path.join(root, `${item.name}-old`);
|
|
const modernTrace = path.join(root, `${item.name}-new`);
|
|
const legacy = spawnSync(
|
|
'/bin/bash',
|
|
[path.join(context.paths.dir_shell, 'task.sh'), 'failure.sh', 'now'],
|
|
{
|
|
cwd: root,
|
|
env: { ...context.env, TRACE: legacyTrace },
|
|
encoding: 'utf8',
|
|
timeout: 15000,
|
|
},
|
|
);
|
|
assert.ifError(legacy.error);
|
|
assert.equal(legacy.status, item.legacy, item.name);
|
|
const env = await sourceEnvironment(
|
|
{ ...context.env, TRACE: modernTrace },
|
|
[context.paths.file_config_user],
|
|
);
|
|
const result = await executeTask(
|
|
{ ...context, env },
|
|
{ argv: ['failure.sh'], mode: 'now' },
|
|
);
|
|
assert.equal(result.exitCode, item.code, item.name);
|
|
const expected = [
|
|
'before',
|
|
...(item.trap ? ['exit-trap'] : []),
|
|
...(item.after ? [`after:${item.code}`] : []),
|
|
];
|
|
for (const file of [legacyTrace, modernTrace])
|
|
assert.deepEqual(
|
|
(await fs.readFile(file, 'utf8')).trim().split('\n'),
|
|
expected,
|
|
`${item.name}: ${file}`,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
await t.test('/dev/null preserves legacy stream routing', async () => {
|
|
await fs.writeFile(
|
|
context.paths.file_task_before,
|
|
'printf "before-out-marker\\n"; printf "before-err-marker\\n" >&2\n',
|
|
);
|
|
await fs.writeFile(
|
|
context.paths.file_task_after,
|
|
'printf "after-out-marker\\n"; printf "after-err-marker\\n" >&2\n',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(context.paths.dir_scripts, 'streams.sh'),
|
|
'printf "child-out-marker\\n"; printf "child-err-marker\\n" >&2\n',
|
|
);
|
|
for (const mode of ['now', 'desi', 'conc']) {
|
|
for (const realtime of [false, true]) {
|
|
await fs.writeFile(
|
|
context.paths.file_config_user,
|
|
`log_name=/dev/null\nno_tee=true\nreal_time=${realtime}\n`,
|
|
);
|
|
const legacy = spawnSync(
|
|
'/bin/bash',
|
|
[
|
|
path.join(context.paths.dir_shell, 'task.sh'),
|
|
'streams.sh',
|
|
mode,
|
|
...(mode === 'now' ? [] : ['ACCOUNTS', '2', '1']),
|
|
],
|
|
{ cwd: root, env: context.env, encoding: 'utf8', timeout: 15000 },
|
|
);
|
|
assert.ifError(legacy.error);
|
|
assert.equal(legacy.status, 0, legacy.stderr);
|
|
const env = await sourceEnvironment(context.env, [
|
|
context.paths.file_config_user,
|
|
]);
|
|
let output = '';
|
|
const result = await executeTask(
|
|
{ ...context, env },
|
|
{
|
|
argv: ['streams.sh'],
|
|
mode,
|
|
variable: mode === 'now' ? undefined : 'ACCOUNTS',
|
|
selection: mode === 'now' ? undefined : '2 1',
|
|
output: (chunk) => {
|
|
output += chunk;
|
|
},
|
|
},
|
|
);
|
|
assert.equal(result.exitCode, 0);
|
|
assert.equal(result.logPath, '/dev/null');
|
|
const markers = (text) =>
|
|
text.match(/(?:before|after|child)-(?:out|err)-marker/g) ?? [];
|
|
assert.deepEqual(markers(legacy.stdout), []);
|
|
assert.deepEqual(
|
|
markers(output),
|
|
markers(legacy.stderr),
|
|
`${mode}, realtime=${realtime}`,
|
|
);
|
|
assert.ok(markers(output).includes('before-err-marker'));
|
|
assert.equal(
|
|
markers(output).includes('child-err-marker'),
|
|
mode !== 'conc',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
test('empty ignored-minute settings match legacy random delay at minute zero', async (t) => {
|
|
const source = await fs.readFile(
|
|
path.resolve(__dirname, '../../../shell/otask.sh'),
|
|
'utf8',
|
|
);
|
|
const legacyFunction = source.match(/random_delay\(\) \{[\s\S]*?\n\}/)[0];
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-delay-parity-'));
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
await fs.mkdir(path.join(root, 'data/scripts'), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(root, 'data/scripts/fixture.sh'),
|
|
'printf TASK_RAN',
|
|
);
|
|
const OriginalDate = global.Date;
|
|
class MinuteZeroDate extends OriginalDate {
|
|
getMinutes() {
|
|
return 0;
|
|
}
|
|
}
|
|
global.Date = MinuteZeroDate;
|
|
t.after(() => {
|
|
global.Date = OriginalDate;
|
|
});
|
|
for (const ignored of [undefined, '', ' ', '0', ' 30 ']) {
|
|
const env = {
|
|
PATH: process.env.PATH,
|
|
RandomDelay: '1',
|
|
RandomDelayFileExtensions: 'sh',
|
|
...(ignored === undefined ? {} : { RandomDelayIgnoredMinutes: ignored }),
|
|
};
|
|
const legacy = execFileSync(
|
|
'/bin/bash',
|
|
[
|
|
'-c',
|
|
`${legacyFunction}
|
|
date() { printf 0; }
|
|
gen_random_num() { printf 0; }
|
|
t() { :; }
|
|
sleep() { printf DELAYED; }
|
|
random_delay fixture.sh
|
|
`,
|
|
],
|
|
{ env, encoding: 'utf8' },
|
|
);
|
|
let output = '';
|
|
const result = await executeTask(createContext({ root }, env), {
|
|
argv: ['fixture.sh'],
|
|
output: (chunk) => {
|
|
output += chunk.toString();
|
|
},
|
|
});
|
|
assert.equal(result.exitCode, 0, output);
|
|
assert.match(output, /TASK_RAN/);
|
|
assert.equal(
|
|
output.includes('任务随机延迟'),
|
|
legacy.includes('DELAYED'),
|
|
`ignored=${JSON.stringify(ignored)}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('random delay follows legacy dispatch for script arguments and executable commands', async (t) => {
|
|
const source = await fs.readFile(
|
|
path.resolve(__dirname, '../../../shell/otask.sh'),
|
|
'utf8',
|
|
);
|
|
const functions = ['random_delay', 'run_normal', 'main']
|
|
.map(
|
|
(name) =>
|
|
source.match(new RegExp(`${name}\\(\\) \\{[\\s\\S]*?\\n\\}`))[0],
|
|
)
|
|
.join('\n');
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-delay-dispatch-'));
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
await fs.mkdir(path.join(root, 'data/scripts'), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(root, 'data/scripts/fixture.sh'),
|
|
'printf TASK_RAN',
|
|
);
|
|
const env = {
|
|
PATH: process.env.PATH,
|
|
RandomDelay: '1',
|
|
RandomDelayFileExtensions: '',
|
|
RandomDelayIgnoredMinutes: '99',
|
|
};
|
|
for (const args of [
|
|
['fixture.sh'],
|
|
['fixture.sh', 'argument'],
|
|
['fixture.sh', 'now'],
|
|
['true'],
|
|
]) {
|
|
const legacy = execFileSync(
|
|
'/bin/bash',
|
|
[
|
|
'-c',
|
|
`${functions}
|
|
date() { printf 0; }
|
|
gen_random_num() { printf 0; }
|
|
t() { :; }
|
|
sleep() { printf DELAYED; }
|
|
enter_script_workdir() { :; }
|
|
run_else() { :; }
|
|
which_program=true
|
|
main "$@"
|
|
`,
|
|
'fixture',
|
|
...args,
|
|
],
|
|
{ env, encoding: 'utf8' },
|
|
);
|
|
let output = '';
|
|
const parsed = require('../../dist/runner').parseExecution(args);
|
|
const result = await executeTask(createContext({ root }, env), {
|
|
...parsed.execution,
|
|
output: (chunk) => {
|
|
output += chunk.toString();
|
|
},
|
|
});
|
|
assert.equal(result.exitCode, 0, output);
|
|
assert.equal(
|
|
output.includes('任务随机延迟'),
|
|
legacy.includes('DELAYED'),
|
|
JSON.stringify(args),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('configuration shell options survive the environment bridge for pipeline and glob behavior', async (t) => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-options-'));
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
const config = path.join(root, 'config.sh');
|
|
await fs.writeFile(config, 'set -o pipefail\nshopt -s nullglob\n');
|
|
const probe =
|
|
'false | true; printf "pipeline=%s\\n" "$?"; files=("$EMPTY_DIR"/*.missing); printf "matches=%s\\n" "${#files[@]}"';
|
|
const base = { PATH: process.env.PATH, EMPTY_DIR: root };
|
|
const legacy = execFileSync(
|
|
'bash',
|
|
[
|
|
'--noprofile',
|
|
'--norc',
|
|
'-c',
|
|
'. "$1"; eval "$2"',
|
|
'fixture',
|
|
config,
|
|
probe,
|
|
],
|
|
{ env: base, encoding: 'utf8' },
|
|
);
|
|
const env = await sourceEnvironment(base, [config]);
|
|
assert.ok(env.SHELLOPTS.split(':').includes('pipefail'));
|
|
assert.ok(!env.SHELLOPTS.split(':').includes('allexport'));
|
|
// Bash 3.x cannot import BASHOPTS itself; exercise the actual CLI bridge.
|
|
const execution = await require('../../dist/internal/runtime/process').runProcess(
|
|
'bash', ['--noprofile', '--norc', '-c', probe], { env, capture: true },
|
|
);
|
|
assert.equal(execution.code, 0);
|
|
const modern = execution.stdout;
|
|
assert.equal(modern, legacy);
|
|
assert.match(modern, /pipeline=1/);
|
|
const context = createContext({ root }, { ...env, no_tee: 'true' });
|
|
await fs.mkdir(context.paths.dir_scripts, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(context.paths.dir_scripts, 'pipeline.sh'),
|
|
'false | true\n',
|
|
);
|
|
const result = await executeTask(context, {
|
|
argv: ['pipeline.sh'],
|
|
mode: 'now',
|
|
});
|
|
assert.equal(result.exitCode, 1);
|
|
});
|
|
|
|
test('configuration errexit and nounset retain task and after-hook semantics', async (t) => {
|
|
for (const option of ['errexit', 'nounset']) {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-option-task-'));
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
const config = path.join(root, 'config.sh');
|
|
await fs.writeFile(config, `set -o ${option}\n`);
|
|
const env = await sourceEnvironment({ PATH: process.env.PATH }, [config]);
|
|
assert.ok(env.SHELLOPTS.split(':').includes(option));
|
|
const context = createContext({ root }, { ...env, no_tee: 'true' });
|
|
await fs.mkdir(context.paths.dir_scripts, { recursive: true });
|
|
await fs.mkdir(context.paths.dir_config, { recursive: true });
|
|
const marker = path.join(root, 'after');
|
|
context.env.AFTER_MARKER = marker;
|
|
await fs.writeFile(
|
|
context.paths.file_task_after,
|
|
'printf "%s" "$-" > "$AFTER_MARKER"\n',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(context.paths.dir_scripts, 'options.sh'),
|
|
option === 'errexit'
|
|
? 'false\nprintf SHOULD_NOT_RUN\n'
|
|
: 'printf "%s" "$QL_UNSET_FIXTURE"\n',
|
|
);
|
|
const result = await executeTask(context, {
|
|
argv: ['options.sh'],
|
|
mode: 'now',
|
|
});
|
|
if (option === 'errexit') {
|
|
assert.equal(result.exitCode, 1);
|
|
await assert.rejects(fs.access(marker));
|
|
assert.doesNotMatch(
|
|
await fs.readFile(
|
|
path.join(context.paths.dir_log, result.logPath),
|
|
'utf8',
|
|
),
|
|
/SHOULD_NOT_RUN/,
|
|
);
|
|
} else {
|
|
assert.equal(result.exitCode, 0);
|
|
assert.match(await fs.readFile(marker, 'utf8'), /u/);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('later hooks can disable previously enabled shell options without mutating the prior snapshot', async (t) => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-option-reset-'));
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
const config = path.join(root, 'config.sh');
|
|
const hook = path.join(root, 'before.sh');
|
|
await fs.writeFile(config, 'set -o pipefail\nshopt -s nullglob\n');
|
|
await fs.writeFile(hook, 'set +o pipefail\nshopt -u nullglob\n');
|
|
const base = { PATH: process.env.PATH, EMPTY_DIR: root };
|
|
const enabled = await sourceEnvironment(base, [config]);
|
|
const disabled = await sourceEnvironment(enabled, [hook]);
|
|
assert.ok(enabled.SHELLOPTS.split(':').includes('pipefail'));
|
|
assert.ok(!disabled.SHELLOPTS.split(':').includes('pipefail'));
|
|
assert.ok(!disabled.BASHOPTS.split(':').includes('nullglob'));
|
|
const probe =
|
|
'false | true; printf "pipeline=%s\\n" "$?"; files=("$EMPTY_DIR"/*.missing); printf "matches=%s\\n" "${#files[@]}"';
|
|
const legacy = execFileSync(
|
|
'bash',
|
|
[
|
|
'--noprofile',
|
|
'--norc',
|
|
'-c',
|
|
'. "$1"; . "$2"; eval "$3"',
|
|
'fixture',
|
|
config,
|
|
hook,
|
|
probe,
|
|
],
|
|
{ env: base, encoding: 'utf8' },
|
|
);
|
|
const modern = execFileSync('bash', ['--noprofile', '--norc', '-c', probe], {
|
|
env: disabled,
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(modern, legacy);
|
|
assert.equal(modern, 'pipeline=0\nmatches=1\n');
|
|
const context = createContext({ root }, { ...enabled, no_tee: 'true' });
|
|
await fs.mkdir(context.paths.dir_scripts, { recursive: true });
|
|
await fs.mkdir(context.paths.dir_config, { recursive: true });
|
|
await fs.copyFile(hook, context.paths.file_task_before);
|
|
await fs.writeFile(
|
|
path.join(context.paths.dir_scripts, 'pipeline.sh'),
|
|
'false | true\n',
|
|
);
|
|
await fs.writeFile(
|
|
context.paths.file_task_after,
|
|
'false | true; printf "%s" "$?" > "$AFTER_RESULT"\n',
|
|
);
|
|
context.env.AFTER_RESULT = path.join(root, 'after-result');
|
|
const result = await executeTask(context, {
|
|
argv: ['pipeline.sh'],
|
|
mode: 'now',
|
|
});
|
|
assert.equal(result.exitCode, 0);
|
|
assert.equal(await fs.readFile(context.env.AFTER_RESULT, 'utf8'), '0');
|
|
});
|
|
|
|
test('disabled default shell options stay disabled across configuration and task bridges', async (t) => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-default-options-'));
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
const config = path.join(root, 'config.sh');
|
|
await fs.writeFile(config, 'set +o braceexpand\nshopt -u extquote\n');
|
|
const env = await sourceEnvironment({ PATH: process.env.PATH }, [config]);
|
|
const refreshed = await sourceEnvironment(env, []);
|
|
assert.ok(!refreshed.SHELLOPTS.split(':').includes('braceexpand'));
|
|
assert.ok(!refreshed.BASHOPTS.split(':').includes('extquote'));
|
|
const context = createContext({ root }, { ...refreshed, no_tee: 'true' });
|
|
await fs.mkdir(context.paths.dir_scripts, { recursive: true });
|
|
const script =
|
|
'printf "%s\\n" {a,b}; shopt -q extquote && printf WRONG; true\n';
|
|
await fs.writeFile(
|
|
path.join(context.paths.dir_scripts, 'options.sh'),
|
|
script,
|
|
);
|
|
const legacy = execFileSync(
|
|
'bash',
|
|
[
|
|
'--noprofile',
|
|
'--norc',
|
|
'-c',
|
|
'. "$1"; eval "$2"',
|
|
'fixture',
|
|
config,
|
|
script,
|
|
],
|
|
{ env: { PATH: process.env.PATH }, encoding: 'utf8' },
|
|
);
|
|
assert.equal(legacy, '{a,b}\n');
|
|
const result = await executeTask(context, {
|
|
argv: ['options.sh'],
|
|
mode: 'now',
|
|
});
|
|
assert.equal(result.exitCode, 0);
|
|
const log = await fs.readFile(
|
|
path.join(context.paths.dir_log, result.logPath),
|
|
'utf8',
|
|
);
|
|
assert.ok(log.includes(legacy));
|
|
assert.doesNotMatch(log, /WRONG/);
|
|
});
|