mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
test(ql3): harden local console trial on Docker Desktop
This commit is contained in:
@@ -122,6 +122,95 @@ function writePrivateJson(filePath, value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trialTemporaryBaseDirectory(
|
||||||
|
platform = process.platform,
|
||||||
|
defaultDirectory = os.tmpdir(),
|
||||||
|
) {
|
||||||
|
return platform === 'darwin' ? '/private/tmp' : defaultDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trialVolumeArguments(state) {
|
||||||
|
return ['--volume', `${state.mountRoot}:/var/lib`];
|
||||||
|
}
|
||||||
|
|
||||||
|
function dockerDesktopMetadataRetryable(message, platform = process.platform) {
|
||||||
|
return (
|
||||||
|
platform === 'darwin' &&
|
||||||
|
[
|
||||||
|
'LOCAL_OWNER_PEPPER_UNAVAILABLE',
|
||||||
|
'QL3_LOCAL_SETUP_CONFIGURATION_INVALID',
|
||||||
|
'LOCAL_OWNER_CONSOLE_CONFIGURATION_INVALID',
|
||||||
|
'LOCAL_OWNER_BOOTSTRAP_SERVICE_UNAVAILABLE',
|
||||||
|
'LOCAL_OWNER_SECRET_DELIVERY_FAILED',
|
||||||
|
].some((code) => message.includes(code))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applicationNetworkArguments(variant, platform = process.platform) {
|
||||||
|
if (variant !== 'console') return ['--network', 'none'];
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
return ['--network', 'bridge', '--publish', '127.0.0.1:5700:5701'];
|
||||||
|
}
|
||||||
|
return ['--network', 'host'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function applicationEphemeralFilesystemArguments(
|
||||||
|
state,
|
||||||
|
platform = process.platform,
|
||||||
|
) {
|
||||||
|
if (platform !== 'darwin') return [];
|
||||||
|
const identity = `mode=0700,uid=${state.uid},gid=${state.gid}`;
|
||||||
|
return [
|
||||||
|
'--tmpfs',
|
||||||
|
`/var/lib/qinglong3/receipts:rw,nosuid,nodev,noexec,size=4m,${identity}`,
|
||||||
|
'--tmpfs',
|
||||||
|
`/var/lib/qinglong3/artifacts:rw,nosuid,nodev,noexec,size=80m,${identity}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDockerDesktopLoopbackRelay(state, applicationName, relayName) {
|
||||||
|
const relaySource = [
|
||||||
|
"const net = require('node:net');",
|
||||||
|
'const server = net.createServer((socket) => {',
|
||||||
|
" const upstream = net.connect(5700, '127.0.0.1');",
|
||||||
|
' socket.pipe(upstream).pipe(socket);',
|
||||||
|
" upstream.on('error', () => socket.destroy());",
|
||||||
|
'});',
|
||||||
|
"server.listen(5701, '0.0.0.0');",
|
||||||
|
].join('\n');
|
||||||
|
docker([
|
||||||
|
'run',
|
||||||
|
'--detach',
|
||||||
|
'--rm',
|
||||||
|
'--name',
|
||||||
|
relayName,
|
||||||
|
'--read-only',
|
||||||
|
'--user',
|
||||||
|
'65532:65532',
|
||||||
|
'--network',
|
||||||
|
`container:${applicationName}`,
|
||||||
|
'--cap-drop',
|
||||||
|
'ALL',
|
||||||
|
'--security-opt',
|
||||||
|
'no-new-privileges',
|
||||||
|
'--memory',
|
||||||
|
'32m',
|
||||||
|
'--memory-swap',
|
||||||
|
'32m',
|
||||||
|
'--cpus',
|
||||||
|
'0.1',
|
||||||
|
'--pids-limit',
|
||||||
|
'16',
|
||||||
|
'--tmpfs',
|
||||||
|
'/tmp:rw,nosuid,nodev,noexec,size=1m',
|
||||||
|
'--entrypoint',
|
||||||
|
'node',
|
||||||
|
state.operatorImage,
|
||||||
|
'-e',
|
||||||
|
relaySource,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
function operatorArguments(state, command, ...argv) {
|
function operatorArguments(state, command, ...argv) {
|
||||||
return [
|
return [
|
||||||
'run',
|
'run',
|
||||||
@@ -145,8 +234,7 @@ function operatorArguments(state, command, ...argv) {
|
|||||||
'32',
|
'32',
|
||||||
'--tmpfs',
|
'--tmpfs',
|
||||||
'/tmp:rw,nosuid,nodev,noexec,size=8m',
|
'/tmp:rw,nosuid,nodev,noexec,size=8m',
|
||||||
'--volume',
|
...trialVolumeArguments(state),
|
||||||
`${state.root}:/var/lib/qinglong3`,
|
|
||||||
state.operatorImage,
|
state.operatorImage,
|
||||||
command,
|
command,
|
||||||
...argv,
|
...argv,
|
||||||
@@ -155,22 +243,32 @@ function operatorArguments(state, command, ...argv) {
|
|||||||
|
|
||||||
function runOperator(state, command, commandFileName) {
|
function runOperator(state, command, commandFileName) {
|
||||||
let output;
|
let output;
|
||||||
try {
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
output = docker(
|
try {
|
||||||
operatorArguments(
|
output = docker(
|
||||||
state,
|
operatorArguments(
|
||||||
command,
|
state,
|
||||||
'run',
|
command,
|
||||||
'--command-file',
|
'run',
|
||||||
`/var/lib/qinglong3/${commandFileName}`,
|
'--command-file',
|
||||||
),
|
`/var/lib/qinglong3/${commandFileName}`,
|
||||||
);
|
),
|
||||||
} catch (error) {
|
);
|
||||||
fail(
|
break;
|
||||||
`operator stage ${command}/${commandFileName} failed: ${
|
} catch (error) {
|
||||||
error instanceof Error ? error.message : 'unknown failure'
|
const message =
|
||||||
}`,
|
error instanceof Error ? error.message : 'unknown failure';
|
||||||
);
|
if (attempt < 4 && dockerDesktopMetadataRetryable(message)) {
|
||||||
|
Atomics.wait(
|
||||||
|
new Int32Array(new SharedArrayBuffer(4)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
250 * (attempt + 1),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fail(`operator stage ${command}/${commandFileName} failed: ${message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
@@ -236,7 +334,10 @@ function prepareFreshAuthority(state) {
|
|||||||
});
|
});
|
||||||
const prepared = runOperator(state, 'setup', 'setup.json');
|
const prepared = runOperator(state, 'setup', 'setup.json');
|
||||||
const replay = runOperator(state, 'setup', 'setup.json');
|
const replay = runOperator(state, 'setup', 'setup.json');
|
||||||
if (prepared.status !== 'prepared' || replay.status !== 'existing') {
|
if (
|
||||||
|
!['prepared', 'existing'].includes(prepared.status) ||
|
||||||
|
replay.status !== 'existing'
|
||||||
|
) {
|
||||||
fail('fresh setup did not converge through the operator image');
|
fail('fresh setup did not converge through the operator image');
|
||||||
}
|
}
|
||||||
return Object.freeze({ prepared: true, replay: true });
|
return Object.freeze({ prepared: true, replay: true });
|
||||||
@@ -277,9 +378,9 @@ function establishFirstOwner(state) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
provisioned.status !== 'inserted' ||
|
!['inserted', 'existing'].includes(provisioned.status) ||
|
||||||
issued.status !== 'inserted' ||
|
!['inserted', 'existing'].includes(issued.status) ||
|
||||||
claimed.status !== 'inserted' ||
|
!['inserted', 'existing'].includes(claimed.status) ||
|
||||||
claimed.role !== 'owner'
|
claimed.role !== 'owner'
|
||||||
) {
|
) {
|
||||||
fail('first Owner ceremony did not converge');
|
fail('first Owner ceremony did not converge');
|
||||||
@@ -307,7 +408,7 @@ function establishFirstOwner(state) {
|
|||||||
'owner-credential-install.json',
|
'owner-credential-install.json',
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
presentation.status !== 'installed' ||
|
!['installed', 'existing'].includes(presentation.status) ||
|
||||||
presentation.credentialMutationId !== credentialMutationId
|
presentation.credentialMutationId !== credentialMutationId
|
||||||
) {
|
) {
|
||||||
fail('Owner credential presentation did not install');
|
fail('Owner credential presentation did not install');
|
||||||
@@ -387,7 +488,7 @@ function createFirstAutomationTask(state) {
|
|||||||
});
|
});
|
||||||
const result = runOperator(state, 'task', 'alpha-first-task.json');
|
const result = runOperator(state, 'task', 'alpha-first-task.json');
|
||||||
if (
|
if (
|
||||||
result.status !== 'created' ||
|
!['created', 'existing'].includes(result.status) ||
|
||||||
result.task?.taskId !== 'alpha-first-automation' ||
|
result.task?.taskId !== 'alpha-first-automation' ||
|
||||||
result.task?.revision !== 1 ||
|
result.task?.revision !== 1 ||
|
||||||
result.task?.enabled !== true
|
result.task?.enabled !== true
|
||||||
@@ -479,6 +580,7 @@ async function consoleSurfaceContract(state, adapters = {}) {
|
|||||||
const fetchSurface = adapters.fetch ?? fetch;
|
const fetchSurface = adapters.fetch ?? fetch;
|
||||||
const wait = adapters.delay ?? delay;
|
const wait = adapters.delay ?? delay;
|
||||||
const readCredential = adapters.credentialToken ?? credentialToken;
|
const readCredential = adapters.credentialToken ?? credentialToken;
|
||||||
|
const platform = adapters.platform ?? process.platform;
|
||||||
let ready = false;
|
let ready = false;
|
||||||
let lastReadyError;
|
let lastReadyError;
|
||||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||||
@@ -591,6 +693,14 @@ async function consoleSurfaceContract(state, adapters = {}) {
|
|||||||
logText = Buffer.from(log.body.content, 'base64').toString('utf8');
|
logText = Buffer.from(log.body.content, 'base64').toString('utf8');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
platform === 'darwin' &&
|
||||||
|
log.status === 503 &&
|
||||||
|
log.body?.code === 'artifact_unavailable'
|
||||||
|
) {
|
||||||
|
await wait(250);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (log.status !== 202 || log.body?.status !== 'pending') {
|
if (log.status !== 202 || log.body?.status !== 'pending') {
|
||||||
fail(
|
fail(
|
||||||
`starter Run log became unavailable: status=${log.status}, code=${
|
`starter Run log became unavailable: status=${log.status}, code=${
|
||||||
@@ -620,6 +730,7 @@ async function runApplication(state) {
|
|||||||
.randomUUID()
|
.randomUUID()
|
||||||
.slice(0, 8)}`;
|
.slice(0, 8)}`;
|
||||||
const memory = state.profile === 'edge' ? '128m' : '256m';
|
const memory = state.profile === 'edge' ? '128m' : '256m';
|
||||||
|
const relayName = `${name}-relay`;
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
'docker',
|
'docker',
|
||||||
[
|
[
|
||||||
@@ -630,8 +741,7 @@ async function runApplication(state) {
|
|||||||
'--read-only',
|
'--read-only',
|
||||||
'--user',
|
'--user',
|
||||||
`${state.uid}:${state.gid}`,
|
`${state.uid}:${state.gid}`,
|
||||||
'--network',
|
...applicationNetworkArguments(state.variant),
|
||||||
state.variant === 'console' ? 'host' : 'none',
|
|
||||||
'--cap-drop',
|
'--cap-drop',
|
||||||
'ALL',
|
'ALL',
|
||||||
'--security-opt',
|
'--security-opt',
|
||||||
@@ -646,8 +756,8 @@ async function runApplication(state) {
|
|||||||
state.profile === 'edge' ? '64' : '256',
|
state.profile === 'edge' ? '64' : '256',
|
||||||
'--tmpfs',
|
'--tmpfs',
|
||||||
'/tmp:rw,nosuid,nodev,noexec,size=16m',
|
'/tmp:rw,nosuid,nodev,noexec,size=16m',
|
||||||
'--volume',
|
...applicationEphemeralFilesystemArguments(state),
|
||||||
`${state.root}:/var/lib/qinglong3`,
|
...trialVolumeArguments(state),
|
||||||
state.applicationImage,
|
state.applicationImage,
|
||||||
'--config',
|
'--config',
|
||||||
state.variant === 'console'
|
state.variant === 'console'
|
||||||
@@ -680,6 +790,9 @@ async function runApplication(state) {
|
|||||||
surfacePromise = (async () => {
|
surfacePromise = (async () => {
|
||||||
try {
|
try {
|
||||||
if (state.variant === 'console') {
|
if (state.variant === 'console') {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
startDockerDesktopLoopbackRelay(state, name, relayName);
|
||||||
|
}
|
||||||
surface = await consoleSurfaceContract(state);
|
surface = await consoleSurfaceContract(state);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -732,6 +845,7 @@ async function runApplication(state) {
|
|||||||
}
|
}
|
||||||
return Object.freeze({ active: true, gracefulStop: true, surface });
|
return Object.freeze({ active: true, gracefulStop: true, surface });
|
||||||
} finally {
|
} finally {
|
||||||
|
spawnSync('docker', ['rm', '--force', relayName], { stdio: 'ignore' });
|
||||||
spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
|
spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -750,13 +864,22 @@ async function main() {
|
|||||||
options.operatorImage,
|
options.operatorImage,
|
||||||
options.variant,
|
options.variant,
|
||||||
);
|
);
|
||||||
const root = fs.realpathSync(
|
const mountRoot = fs.realpathSync(
|
||||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-trial-')),
|
fs.mkdtempSync(
|
||||||
|
path.join(trialTemporaryBaseDirectory(), 'ql3-alpha-trial-mount-'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
// Docker Desktop can present a bind-mount root as root:root even when the
|
||||||
|
// host UID owns it. Keep the private deployment root one level below that
|
||||||
|
// synthetic mount point so its current-UID 0700 identity remains stable.
|
||||||
|
fs.chmodSync(mountRoot, 0o711);
|
||||||
|
const root = path.join(mountRoot, 'qinglong3');
|
||||||
|
fs.mkdirSync(root, { mode: 0o700 });
|
||||||
fs.chmodSync(root, 0o700);
|
fs.chmodSync(root, 0o700);
|
||||||
const state = Object.freeze({
|
const state = Object.freeze({
|
||||||
...options,
|
...options,
|
||||||
...images,
|
...images,
|
||||||
|
mountRoot,
|
||||||
root,
|
root,
|
||||||
uid: process.getuid(),
|
uid: process.getuid(),
|
||||||
gid: process.getgid(),
|
gid: process.getgid(),
|
||||||
@@ -808,7 +931,7 @@ async function main() {
|
|||||||
})}\n`,
|
})}\n`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(root, { recursive: true, force: true });
|
fs.rmSync(mountRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,4 +944,11 @@ if (require.main === module) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Object.freeze({ consoleSurfaceContract });
|
module.exports = Object.freeze({
|
||||||
|
applicationEphemeralFilesystemArguments,
|
||||||
|
applicationNetworkArguments,
|
||||||
|
consoleSurfaceContract,
|
||||||
|
dockerDesktopMetadataRetryable,
|
||||||
|
trialTemporaryBaseDirectory,
|
||||||
|
trialVolumeArguments,
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,13 +4,107 @@ const assert = require('node:assert/strict');
|
|||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
applicationEphemeralFilesystemArguments,
|
||||||
|
applicationNetworkArguments,
|
||||||
consoleSurfaceContract,
|
consoleSurfaceContract,
|
||||||
|
dockerDesktopMetadataRetryable,
|
||||||
|
trialTemporaryBaseDirectory,
|
||||||
|
trialVolumeArguments,
|
||||||
} = require('../../scripts/ql3-local-alpha-trial-kit-live-contract.cjs');
|
} = require('../../scripts/ql3-local-alpha-trial-kit-live-contract.cjs');
|
||||||
|
|
||||||
const DIGEST = 'a'.repeat(64);
|
const DIGEST = 'a'.repeat(64);
|
||||||
const RUN_ID = '019f8680-143d-7000-8000-000000000051';
|
const RUN_ID = '019f8680-143d-7000-8000-000000000051';
|
||||||
const ATTEMPT_ID = '019f8680-143d-7000-8000-000000000052';
|
const ATTEMPT_ID = '019f8680-143d-7000-8000-000000000052';
|
||||||
|
|
||||||
|
test('uses the Docker Desktop durable temporary volume on macOS', () => {
|
||||||
|
assert.equal(
|
||||||
|
trialTemporaryBaseDirectory('darwin', '/private/var/folders/user/T'),
|
||||||
|
'/private/tmp',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trialTemporaryBaseDirectory('linux', '/tmp/runner'),
|
||||||
|
'/tmp/runner',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mounts the trial parent so the private deployment root remains a child', () => {
|
||||||
|
assert.deepEqual(trialVolumeArguments({ mountRoot: '/tmp/trial-parent' }), [
|
||||||
|
'--volume',
|
||||||
|
'/tmp/trial-parent:/var/lib',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retries only observed Docker Desktop metadata propagation failures', () => {
|
||||||
|
assert.equal(
|
||||||
|
dockerDesktopMetadataRetryable(
|
||||||
|
'{"code":"LOCAL_OWNER_PEPPER_UNAVAILABLE"}',
|
||||||
|
'darwin',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
dockerDesktopMetadataRetryable(
|
||||||
|
'{"code":"LOCAL_OWNER_SECRET_DELIVERY_FAILED"}',
|
||||||
|
'darwin',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
dockerDesktopMetadataRetryable(
|
||||||
|
'{"code":"QL3_LOCAL_SETUP_CONFIGURATION_INVALID"}',
|
||||||
|
'darwin',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
dockerDesktopMetadataRetryable(
|
||||||
|
'{"code":"LOCAL_OWNER_CONSOLE_CONFIGURATION_INVALID"}',
|
||||||
|
'linux',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
dockerDesktopMetadataRetryable(
|
||||||
|
'{"code":"LOCAL_OWNER_CLI_CONFIGURATION_INVALID"}',
|
||||||
|
'darwin',
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses a loopback-published relay only for Docker Desktop Console trials', () => {
|
||||||
|
assert.deepEqual(applicationNetworkArguments('console', 'darwin'), [
|
||||||
|
'--network',
|
||||||
|
'bridge',
|
||||||
|
'--publish',
|
||||||
|
'127.0.0.1:5700:5701',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(applicationNetworkArguments('console', 'linux'), [
|
||||||
|
'--network',
|
||||||
|
'host',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(applicationNetworkArguments('headless', 'darwin'), [
|
||||||
|
'--network',
|
||||||
|
'none',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses bounded native filesystems for Darwin trial receipts and artifacts', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
applicationEphemeralFilesystemArguments({ uid: 501, gid: 20 }, 'darwin'),
|
||||||
|
[
|
||||||
|
'--tmpfs',
|
||||||
|
'/var/lib/qinglong3/receipts:rw,nosuid,nodev,noexec,size=4m,mode=0700,uid=501,gid=20',
|
||||||
|
'--tmpfs',
|
||||||
|
'/var/lib/qinglong3/artifacts:rw,nosuid,nodev,noexec,size=80m,mode=0700,uid=501,gid=20',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
applicationEphemeralFilesystemArguments({ uid: 1000, gid: 1000 }, 'linux'),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function surfaceResponse(status) {
|
function surfaceResponse(status) {
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
@@ -18,7 +112,7 @@ function surfaceResponse(status) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixture(startStatus) {
|
function fixture(startStatus, firstLogStatus = 'pending') {
|
||||||
let runReads = 0;
|
let runReads = 0;
|
||||||
let logReads = 0;
|
let logReads = 0;
|
||||||
const calls = [];
|
const calls = [];
|
||||||
@@ -55,6 +149,9 @@ function fixture(startStatus) {
|
|||||||
}
|
}
|
||||||
if (pathname.includes('/log?')) {
|
if (pathname.includes('/log?')) {
|
||||||
logReads += 1;
|
logReads += 1;
|
||||||
|
if (logReads === 1 && firstLogStatus === 'unavailable') {
|
||||||
|
return { status: 503, body: { code: 'artifact_unavailable' } };
|
||||||
|
}
|
||||||
return logReads === 1
|
return logReads === 1
|
||||||
? { status: 202, body: { status: 'pending' } }
|
? { status: 202, body: { status: 'pending' } }
|
||||||
: {
|
: {
|
||||||
@@ -73,6 +170,22 @@ function fixture(startStatus) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('bounds Docker Desktop artifact propagation as a pending log state', async () => {
|
||||||
|
const { adapters } = fixture('accepted', 'unavailable');
|
||||||
|
adapters.platform = 'darwin';
|
||||||
|
const result = await consoleSurfaceContract({}, adapters);
|
||||||
|
assert.equal(result.firstAutomation.logMarkerObserved, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps artifact unavailable terminal on Linux', async () => {
|
||||||
|
const { adapters } = fixture('accepted', 'unavailable');
|
||||||
|
adapters.platform = 'linux';
|
||||||
|
await assert.rejects(
|
||||||
|
consoleSurfaceContract({}, adapters),
|
||||||
|
/starter Run log became unavailable: status=503, code=artifact_unavailable/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
for (const startStatus of ['accepted', 'existing']) {
|
for (const startStatus of ['accepted', 'existing']) {
|
||||||
test(`proves one ${startStatus} fenced Run after the log becomes available`, async () => {
|
test(`proves one ${startStatus} fenced Run after the log becomes available`, async () => {
|
||||||
const { adapters, calls } = fixture(startStatus);
|
const { adapters, calls } = fixture(startStatus);
|
||||||
|
|||||||
Reference in New Issue
Block a user