源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { test } from 'node:test';
|
||||
|
||||
test('a launcher signal cancels preparation before Electron can spawn', async () => {
|
||||
const { createDevelopmentLaunchSession } = await import('./dev-app-runtime.mjs');
|
||||
const signals = new EventEmitter();
|
||||
let finishPreparation;
|
||||
const preparation = new Promise((resolve) => { finishPreparation = resolve; });
|
||||
let finishStopping;
|
||||
const stopped = new Promise((resolve) => { finishStopping = resolve; });
|
||||
const spawns = [];
|
||||
const exits = [];
|
||||
let closes = 0;
|
||||
const session = createDevelopmentLaunchSession({
|
||||
signals,
|
||||
close: async () => { closes += 1; },
|
||||
exit: (code) => {
|
||||
exits.push(code);
|
||||
finishStopping();
|
||||
},
|
||||
});
|
||||
const launching = session.start({
|
||||
prepareMacosDevelopmentLaunch: () => preparation,
|
||||
createDevelopmentLaunchFiles: () => {
|
||||
throw new Error('cancelled launch must not create files');
|
||||
},
|
||||
spawn: (...args) => spawns.push(args),
|
||||
});
|
||||
|
||||
signals.emit('SIGINT');
|
||||
finishPreparation({ appPath: '/tmp/Maka Dev.app', resultFileArgPrefix: '--result=' });
|
||||
|
||||
assert.equal(await launching, null);
|
||||
await stopped;
|
||||
assert.deepEqual(spawns, []);
|
||||
assert.equal(closes, 1);
|
||||
assert.deepEqual(exits, [0]);
|
||||
});
|
||||
|
||||
test('a launcher signal adopts and stops a delayed launch handle exactly once', async () => {
|
||||
const { createDevelopmentLaunchSession } = await import('./dev-app-runtime.mjs');
|
||||
const signals = new EventEmitter();
|
||||
let publishHandle;
|
||||
const pendingHandle = new Promise((resolve) => { publishHandle = resolve; });
|
||||
let finishStopping;
|
||||
const stopped = new Promise((resolve) => { finishStopping = resolve; });
|
||||
let stops = 0;
|
||||
const session = createDevelopmentLaunchSession({
|
||||
signals,
|
||||
startDevelopmentApp: () => pendingHandle,
|
||||
close: async () => {},
|
||||
exit: finishStopping,
|
||||
});
|
||||
const launching = session.start();
|
||||
|
||||
signals.emit('SIGTERM');
|
||||
signals.emit('SIGHUP');
|
||||
publishHandle({ stop: async () => { stops += 1; } });
|
||||
|
||||
await launching;
|
||||
await stopped;
|
||||
assert.equal(stops, 1);
|
||||
});
|
||||
|
||||
test('cancelling during launch preparation prevents any later process spawn', async () => {
|
||||
const { startDevelopmentApp } = await import('./dev-app-runtime.mjs');
|
||||
const controller = new AbortController();
|
||||
let finishPreparation;
|
||||
const preparation = new Promise((resolve) => { finishPreparation = resolve; });
|
||||
const launchFiles = [];
|
||||
const spawns = [];
|
||||
const launching = startDevelopmentApp({
|
||||
signal: controller.signal,
|
||||
prepareMacosDevelopmentLaunch: () => preparation,
|
||||
devSingleInstanceConstants: async () => ({}),
|
||||
createDevelopmentLaunchFiles: () => {
|
||||
launchFiles.push('created');
|
||||
return { logFile: '/tmp/log', resultFile: '/tmp/result' };
|
||||
},
|
||||
spawn: (...args) => {
|
||||
spawns.push(args);
|
||||
return { exitCode: null, killed: false, once: () => {}, kill: () => {} };
|
||||
},
|
||||
});
|
||||
|
||||
controller.abort();
|
||||
finishPreparation({ appPath: '/tmp/Maka Dev.app', resultFileArgPrefix: '--result=' });
|
||||
|
||||
await assert.rejects(launching, (error) => error === controller.signal.reason);
|
||||
assert.deepEqual(launchFiles, []);
|
||||
assert.deepEqual(spawns, []);
|
||||
});
|
||||
|
||||
test('cancelling while the plain launcher loads its lock contract prevents spawn', async () => {
|
||||
const { startDevelopmentApp } = await import('./dev-app-runtime.mjs');
|
||||
const controller = new AbortController();
|
||||
let finishContractLoad;
|
||||
const contract = new Promise((resolve) => { finishContractLoad = resolve; });
|
||||
const spawns = [];
|
||||
const launching = startDevelopmentApp({
|
||||
signal: controller.signal,
|
||||
prepareMacosDevelopmentLaunch: async () => null,
|
||||
devSingleInstanceConstants: () => contract,
|
||||
spawn: (...args) => spawns.push(args),
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
controller.abort();
|
||||
finishContractLoad({ DEV_CONFLICT_HANDLED_BY_LAUNCHER_FLAG: '--handled' });
|
||||
|
||||
await assert.rejects(launching, (error) => error === controller.signal.reason);
|
||||
assert.deepEqual(spawns, []);
|
||||
});
|
||||
|
||||
test('launcher and core agree on the loser contract', async () => {
|
||||
const { devSingleInstanceConstants } = await import('./dev-app-runtime.mjs');
|
||||
const core = await import('@maka/core/dev-single-instance');
|
||||
const launcher = await devSingleInstanceConstants();
|
||||
assert.equal(launcher.DEV_LOSER_EXIT_CODE, core.DEV_LOSER_EXIT_CODE);
|
||||
assert.equal(launcher.DEV_CONFLICT_HANDLED_BY_LAUNCHER_FLAG, core.DEV_CONFLICT_HANDLED_BY_LAUNCHER_FLAG);
|
||||
assert.equal(launcher.DEV_LAUNCH_RESULT_FILE_ARG_PREFIX, core.DEV_LAUNCH_RESULT_FILE_ARG_PREFIX);
|
||||
});
|
||||
|
||||
test('plainLoserExitCode matches only the contract value', async () => {
|
||||
const { plainLoserExitCode } = await import('./dev-app-runtime.mjs');
|
||||
const { DEV_LOSER_EXIT_CODE } = await import('@maka/core/dev-single-instance');
|
||||
assert.equal(await plainLoserExitCode(DEV_LOSER_EXIT_CODE), true);
|
||||
assert.equal(await plainLoserExitCode(0), false);
|
||||
assert.equal(await plainLoserExitCode(1), false);
|
||||
});
|
||||
|
||||
test('loser contract import failure does not claim a loser', async () => {
|
||||
const { plainLoserExitCode } = await import('./dev-app-runtime.mjs');
|
||||
const failingLoader = async () => { throw new Error('dist not built'); };
|
||||
const warnings = [];
|
||||
const origWarn = console.warn;
|
||||
console.warn = (m) => warnings.push(String(m));
|
||||
try {
|
||||
assert.equal(await plainLoserExitCode(42, failingLoader), false);
|
||||
} finally { console.warn = origWarn; }
|
||||
assert.ok(warnings.some((m) => m.includes('dev single-instance constants unavailable')));
|
||||
});
|
||||
|
||||
async function runVerdictCase({ result, stopped = () => false, startupAttempts = 10 }) {
|
||||
const { waitForDevelopmentLaunchVerdict } = await import('./dev-app-runtime.mjs');
|
||||
const { writeFileSync, mkdtempSync, rmSync } = await import('node:fs');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const { join } = await import('node:path');
|
||||
const dir = mkdtempSync(join(tmpdir(), 'maka-abs-'));
|
||||
const resultFile = join(dir, 'launch-result.json');
|
||||
try {
|
||||
if (result) writeFileSync(resultFile, `${JSON.stringify(result)}\n`);
|
||||
return await waitForDevelopmentLaunchVerdict({
|
||||
resultFile,
|
||||
pollMs: 1,
|
||||
startupAttempts,
|
||||
stopped,
|
||||
});
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('verdict wait reports only its own private loser result', async () => {
|
||||
const { waitForDevelopmentLaunchVerdict } = await import('./dev-app-runtime.mjs');
|
||||
let resultChecks = 0;
|
||||
let stopChecks = 0;
|
||||
assert.equal(
|
||||
await waitForDevelopmentLaunchVerdict({
|
||||
readLaunchResult: async () => (++resultChecks >= 2 ? { status: 'loser' } : undefined),
|
||||
delay: async () => {},
|
||||
stopped: () => ++stopChecks > 4,
|
||||
}),
|
||||
'absorbed',
|
||||
);
|
||||
});
|
||||
|
||||
test('the TCC launcher stops supervising after Electron wins the lock', async () => {
|
||||
const { waitForDevelopmentLaunchVerdict } = await import('./dev-app-runtime.mjs');
|
||||
assert.equal(
|
||||
await waitForDevelopmentLaunchVerdict({
|
||||
readLaunchResult: () => ({ status: 'winner' }),
|
||||
isWinnerRunning: () => {
|
||||
throw new Error('the launcher must not observe Electron after the lock verdict');
|
||||
},
|
||||
delay: async () => {},
|
||||
startupAttempts: 1,
|
||||
}),
|
||||
'started',
|
||||
);
|
||||
});
|
||||
|
||||
test('verdict wait marks a loser that never starts as absorbed', async () => {
|
||||
assert.equal(
|
||||
await runVerdictCase({ result: { status: 'loser' }, startupAttempts: 1 }),
|
||||
'absorbed',
|
||||
);
|
||||
});
|
||||
|
||||
test('verdict wait keeps an ordinary never-started launch distinct', async () => {
|
||||
assert.equal(await runVerdictCase({ startupAttempts: 1 }), 'never-started');
|
||||
});
|
||||
|
||||
test('verdict wait marks a stopped loser as absorbed', async () => {
|
||||
let stopChecks = 0;
|
||||
assert.equal(
|
||||
await runVerdictCase({
|
||||
result: { status: 'loser' },
|
||||
stopped: () => ++stopChecks > 1,
|
||||
}),
|
||||
'absorbed',
|
||||
);
|
||||
});
|
||||
|
||||
test('development-app liveness probes only the TCC bundle', async () => {
|
||||
const { isDevelopmentAppRunning } = await import('./dev-app-runtime.mjs');
|
||||
const calls = [];
|
||||
assert.equal(
|
||||
isDevelopmentAppRunning({
|
||||
executable: '/wt/Maka Dev.app/Contents/MacOS/Electron',
|
||||
probe: (executable) => {
|
||||
calls.push(executable);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(calls, ['/wt/Maka Dev.app/Contents/MacOS/Electron']);
|
||||
});
|
||||
|
||||
test('a runtime rebuild refuses to replace a running TCC bundle instead of killing it', async () => {
|
||||
const { assertDevelopmentAppNotRunning } = await import('./dev-app-runtime.mjs');
|
||||
assert.throws(
|
||||
() => assertDevelopmentAppNotRunning({ isRunning: () => true }),
|
||||
/Quit it.*retry/,
|
||||
);
|
||||
assert.doesNotThrow(() => assertDevelopmentAppNotRunning({ isRunning: () => false }));
|
||||
});
|
||||
|
||||
test('TCC launcher cleanup removes only its own launch artifacts', async () => {
|
||||
const { cleanupDevelopmentLaunch } = await import('./dev-app-runtime.mjs');
|
||||
const { existsSync, mkdtempSync, rmSync, writeFileSync } = await import('node:fs');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const { join } = await import('node:path');
|
||||
const directory = mkdtempSync(join(tmpdir(), 'maka-cleanup-'));
|
||||
const logFile = join(directory, 'app.log');
|
||||
const resultFile = join(directory, 'launch-result.json');
|
||||
let logStopped = false;
|
||||
try {
|
||||
writeFileSync(logFile, 'log output\n');
|
||||
writeFileSync(resultFile, '{"status":"winner"}\n');
|
||||
cleanupDevelopmentLaunch({
|
||||
logFile,
|
||||
resultFile,
|
||||
logStream: { kill: () => { logStopped = true; } },
|
||||
});
|
||||
assert.equal(logStopped, true);
|
||||
assert.equal(existsSync(logFile), false);
|
||||
assert.equal(existsSync(resultFile), false);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('each TCC launch gets private and separate result and log files', async () => {
|
||||
const { createDevelopmentLaunchFiles } = await import('./dev-app-runtime.mjs');
|
||||
const writes = [];
|
||||
const first = createDevelopmentLaunchFiles({
|
||||
directory: '/tmp/maka-dev-launches',
|
||||
id: 'first',
|
||||
mkdir: () => {},
|
||||
write: (path) => writes.push(path),
|
||||
});
|
||||
const second = createDevelopmentLaunchFiles({
|
||||
directory: '/tmp/maka-dev-launches',
|
||||
id: 'second',
|
||||
mkdir: () => {},
|
||||
write: (path) => writes.push(path),
|
||||
});
|
||||
assert.notEqual(first.logFile, second.logFile);
|
||||
assert.notEqual(first.resultFile, second.resultFile);
|
||||
assert.deepEqual(writes, [first.logFile, second.logFile]);
|
||||
});
|
||||
|
||||
test('other launch outcomes retain their exit codes', async () => {
|
||||
const { handleDevelopmentLaunchOutcome } = await import('./dev-app-runtime.mjs');
|
||||
for (const [outcome, expectedCode] of [['never-started', 1], ['stopped', 0], ['unexpected', 1]]) {
|
||||
let exitCode;
|
||||
handleDevelopmentLaunchOutcome(outcome, { log: () => {}, exit: (code) => { exitCode = code; } });
|
||||
assert.equal(exitCode, expectedCode, outcome);
|
||||
}
|
||||
let startedExitCode;
|
||||
handleDevelopmentLaunchOutcome('started', { exit: (code) => { startedExitCode = code; } });
|
||||
assert.equal(startedExitCode, undefined);
|
||||
});
|
||||
|
||||
test('shared-profile launches warn about legacy TCC data before choosing plain or bundle mode', async () => {
|
||||
const { DEV_USER_DATA_DIR, warnAboutLegacyTccDataRoot } = await import('./dev-app-runtime.mjs');
|
||||
const warnings = [];
|
||||
warnAboutLegacyTccDataRoot({
|
||||
platform: 'darwin',
|
||||
effectiveUserDataDir: DEV_USER_DATA_DIR,
|
||||
legacyUserDataDir: '/tmp/Maka Dev-legacy',
|
||||
exists: () => true,
|
||||
warn: (message) => warnings.push(message),
|
||||
});
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.match(warnings[0], /Maka Dev-legacy/);
|
||||
});
|
||||
|
||||
test('an explicit isolated profile does not warn about unrelated legacy TCC data', async () => {
|
||||
const { warnAboutLegacyTccDataRoot } = await import('./dev-app-runtime.mjs');
|
||||
const warnings = [];
|
||||
warnAboutLegacyTccDataRoot({
|
||||
platform: 'darwin',
|
||||
effectiveUserDataDir: '/tmp/isolated-profile',
|
||||
legacyUserDataDir: '/tmp/Maka Dev-legacy',
|
||||
exists: () => true,
|
||||
warn: (message) => warnings.push(message),
|
||||
});
|
||||
assert.deepEqual(warnings, []);
|
||||
});
|
||||
|
||||
test('createMacosDevelopmentLaunch passes its private result file after --args', async () => {
|
||||
const { createMacosDevelopmentLaunch } = await import('./dev-app-runtime.mjs');
|
||||
const { DEV_LAUNCH_RESULT_FILE_ARG_PREFIX } = await import('@maka/core/dev-single-instance');
|
||||
const launch = createMacosDevelopmentLaunch(
|
||||
'/tmp/Maka Dev.app',
|
||||
'/tmp/app.log',
|
||||
'/tmp/launch-result.json',
|
||||
DEV_LAUNCH_RESULT_FILE_ARG_PREFIX,
|
||||
);
|
||||
assert.equal(launch.command, 'open');
|
||||
assert.ok(launch.args.indexOf('--stdout') < launch.args.indexOf('--args'));
|
||||
assert.equal(
|
||||
launch.args.at(-1),
|
||||
`${DEV_LAUNCH_RESULT_FILE_ARG_PREFIX}/tmp/launch-result.json`,
|
||||
);
|
||||
assert.ok(
|
||||
!createMacosDevelopmentLaunch('/tmp/Maka Dev.app', '/tmp/app.log').args.includes('--args'),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user