test: add regression scenario coverage

This commit is contained in:
2026-05-08 16:24:45 +00:00
parent 0116c4972d
commit 08ab7fea8e
9 changed files with 1069 additions and 2700 deletions
+7 -6
View File
@@ -6,9 +6,10 @@
"description": "Whole-system integration scenarios for serve.zone components.",
"scripts": {
"bootstrap:components": "pnpm --dir ../interfaces install && pnpm --dir ../api install && pnpm --dir ../cloudly install && pnpm --dir ../corebuild install && pnpm --dir ../coreflow install && pnpm --dir ../coretraffic install && pnpm --dir ../corestore install && pnpm --dir ../onebox exec deno install --config deno.json && deno cache --config ../isocreator/deno.json ../isocreator/mod.ts && deno cache --config ../baseos/deno.json ../baseos/mod.ts && pnpm install",
"test": "pnpm scenario:corestore-volume-driver && pnpm scenario:registry-deploy-on-push && pnpm scenario:onebox-basic-lifecycle && pnpm scenario:onebox-backup-restore",
"test": "pnpm scenario:codebase-regressions && pnpm scenario:corestore-volume-driver && pnpm scenario:registry-deploy-on-push && pnpm scenario:onebox-basic-lifecycle && pnpm scenario:onebox-backup-restore",
"test:full": "pnpm test && pnpm scenario:onebox-cloudly-appstore-worker",
"scenario:corestore-volume-driver": "tsx scenarios/corestore-volume-driver/scenario.ts",
"scenario:codebase-regressions": "tsx scenarios/codebase-regressions/scenario.ts",
"scenario:registry-deploy-on-push": "tsx --tsconfig ../cloudly/tsconfig.json scenarios/registry-deploy-on-push/scenario.ts",
"scenario:baseos-image-pipeline": "tsx --tsconfig ../corebuild/tsconfig.json scenarios/baseos-image-pipeline/scenario.ts",
"scenario:baseos-qemu-enrollment": "tsx scenarios/baseos-qemu-enrollment/scenario.ts",
@@ -22,11 +23,11 @@
"vagrant:destroy": "vagrant destroy -f"
},
"devDependencies": {
"@git.zone/tstest": "2.3.8",
"@push.rocks/smartnetwork": "4.4.0",
"@types/node": "^22.0.0",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
"@git.zone/tstest": "3.6.3",
"@push.rocks/smartnetwork": "4.7.1",
"@types/node": "^25.6.2",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
},
"pnpm": {
"overrides": {
+685 -2656
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -14,6 +14,7 @@ Component repositories keep their fast unit and package tests locally. This repo
| Scenario | Command | What it validates |
| --- | --- | --- |
| `codebase-regressions` | `pnpm scenario:codebase-regressions` | Static cross-package regression checks for security defaults, registry/image stream fixes, package metadata, and ContainerArchive prune safety. |
| `corestore-volume-driver` | `pnpm scenario:corestore-volume-driver` | Corestore control API, Docker VolumeDriver protocol, snapshot/restore through `containerarchive`, Coreflow volume mount generation, backup/restore orchestration, and remote replication against a fake Cloudly API. |
| `registry-deploy-on-push` | `pnpm scenario:registry-deploy-on-push` | Cloudly startup with isolated Mongo/S3 helpers, cluster and registry setup, Docker image push, Cloudly metadata update, Coreflow workload provisioning, Coretraffic HTTPS routing, and same-tag redeploy after digest changes. |
| `onebox-basic-lifecycle` | `pnpm scenario:onebox-basic-lifecycle` | Onebox startup in dev mode, SmartProxy ingress, local registry status, workload deploy/remove, HTTP route check, and HTTPS route check with a temporary certificate. |
@@ -0,0 +1,51 @@
import assert from 'assert/strict';
import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
const readWorkspaceFile = (pathFromRoot: string) => {
return readFileSync(resolve(root, pathFromRoot), 'utf8');
};
const assertIncludes = (filePath: string, expected: string) => {
const content = readWorkspaceFile(filePath);
assert.ok(content.includes(expected), `${filePath} should include ${expected}`);
};
const assertExcludes = (filePath: string, forbidden: string) => {
const content = readWorkspaceFile(filePath);
assert.ok(!content.includes(forbidden), `${filePath} should not include ${forbidden}`);
};
assertIncludes('containerarchive/rust/src/prune.rs', 'rewritten_offsets');
assertExcludes('containerarchive/rust/src/prune.rs', 'offset: entry.offset, // Note: offset in the new pack may differ');
assertIncludes('containerarchive/test/test.ts', 'partial-pack GC rewrites chunks');
assertExcludes('onebox/ts/classes/onebox.ts', "hashPassword('admin')");
assertIncludes('onebox/ts/classes/onebox.ts', 'ONEBOX_ADMIN_PASSWORD');
assertExcludes('dcrouter/ts/opsserver/handlers/admin.handler.ts', "password: 'admin'");
assertIncludes('dcrouter/ts/opsserver/handlers/admin.handler.ts', 'DCROUTER_ADMIN_PASSWORD');
assertExcludes('corerender/ts/rendertron.db.ts', 'wxW4LBa3sxPjyXGf');
assertExcludes('corerender/ts/rendertron.db.ts', 'losslessone-main.zee8suk.mongodb.net');
assertIncludes('api/ts/classes.cloudlyapiclient.ts', 'return response;');
assertIncludes('api/ts/classes.image.ts', 'versionString: imageVersion');
assertIncludes('cloudly/ts/manager.image/classes.imagemanager.ts', 'readFromWebstream(readable)');
assertIncludes('cloudly/ts/manager.service/classes.servicemanager.ts', 'adminIdentityGuard');
assertIncludes('cloudly/ts/manager.cluster/classes.clustermanager.ts', 'getClusterById');
assertIncludes('platformclient/ts/email/classes.emailconnector.ts', 'return response.responseId');
assertIncludes('platformclient/ts/email/classes.letterconnector.ts', 'return response.processId');
assertIncludes('platformclient/ts/email/classes.pushnotificationconnector.ts', "typeof process !== 'undefined'");
assertIncludes('siprouter/ts/sipproxy.ts', 'return { id: callId };');
assertIncludes('onebox/ts/opsserver/classes.opsserver.ts', "'/v2/*'");
assertIncludes('spark/ts/spark.cli.ts', 'CLOUDLY_URL: cloudlyUrl');
const rustdeskConfig = JSON.parse(readWorkspaceFile('appstore-apptemplates/apps/rustdesk-server/versions/1.0.0/config.json'));
const relay = rustdeskConfig.envVars.find((entry: { key: string }) => entry.key === 'RELAY');
assert.equal(relay?.value, '${SERVICE_DOMAIN}:21116');
console.log('codebase regression checks passed');
@@ -1,5 +1,6 @@
import * as http from 'node:http';
import * as net from 'node:net';
import * as crypto from 'node:crypto';
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
@@ -120,6 +121,10 @@ const pathExists = async (pathArg: string) => {
}
};
const sha256Base64 = (base64Arg: string) => {
return crypto.createHash('sha256').update(Buffer.from(base64Arg, 'base64')).digest('hex');
};
const assertCoreflowVolumeMounts = () => {
const coreflow = new Coreflow();
const service: IService = {
@@ -439,12 +444,207 @@ const assertCoreflowBackupOrchestration = async () => {
}
};
const assertCoreflowRemoteReplication = async () => {
const socketPath = join(buildDir, 'replication-plugins', 'corestore.sock');
const dataDir = join(buildDir, 'replication-data');
await mkdir(dirname(socketPath), { recursive: true });
const controlPort = await getFreePort();
const s3Port = await getFreePort();
const dbPort = await getFreePort();
let corestore = new CoreStore({
dataDir,
bindAddress: '127.0.0.1',
publicHost: '127.0.0.1',
controlPort,
s3Port,
dbPort,
apiToken,
volumePluginSocketPath: socketPath,
});
const previousControlUrl = process.env.CORESTORE_CONTROL_URL;
const previousApiToken = process.env.CORESTORE_API_TOKEN;
try {
await corestore.start();
process.env.CORESTORE_CONTROL_URL = `http://127.0.0.1:${controlPort}`;
process.env.CORESTORE_API_TOKEN = apiToken;
const volumeName = 'remote-replication-data';
const service: IService = {
id: 'svc-remote-replication',
data: {
name: 'remote-replication',
description: 'Coreflow remote replication service',
imageId: 'image-remote-replication',
imageVersion: 'latest',
environment: {},
secretBundleId: 'secret-remote-replication',
serviceCategory: 'workload',
deploymentStrategy: 'custom',
scaleFactor: 1,
balancingStrategy: 'round-robin',
ports: {
web: 80,
},
volumes: [
{
source: volumeName,
mountPath: '/data',
backup: true,
},
],
domains: [],
deploymentIds: [],
},
};
const createVolumeResponse = await controlPost<any>(controlPort, '/volumes/create', {
name: volumeName,
serviceId: service.id,
serviceName: service.data.name,
mountPath: '/data',
backup: true,
});
const mountpoint = createVolumeResponse.volume?.Mountpoint;
assert(typeof mountpoint === 'string', `Could not create replication test volume: ${JSON.stringify(createVolumeResponse)}`);
await writeFile(join(mountpoint, 'state.txt'), 'before remote backup\n');
const remoteObjects = new Map<string, { object: any; contentsBase64: string }>();
let remoteManifest: any;
const coreflow = new Coreflow();
const fakeCloudlyApiClient = {
platform: {
getPlatformDesiredState: async () => ({ capabilities: [], providerConfigs: [], bindings: [] }),
},
services: {
getServices: async () => [service],
},
typedsocketClient: {
createTypedRequest: (methodArg: string) => ({
fire: async (payloadArg: any) => {
if (methodArg === 'prepareBackupReplication') {
remoteManifest = payloadArg.manifest;
return {
missingObjects: payloadArg.manifest.objects.filter((objectArg: any) => {
const existing = remoteObjects.get(objectArg.path);
return !existing || existing.object.sha256 !== objectArg.sha256;
}),
};
}
if (methodArg === 'uploadBackupArchiveObject') {
assert(payloadArg.object.sha256 === sha256Base64(payloadArg.contentsBase64), `Remote upload checksum mismatch for ${payloadArg.object.path}`);
remoteObjects.set(payloadArg.object.path, {
object: payloadArg.object,
contentsBase64: payloadArg.contentsBase64,
});
return { accepted: true };
}
if (methodArg === 'completeBackupReplication') {
remoteManifest = payloadArg.manifest;
for (const object of remoteManifest.objects) {
assert(remoteObjects.has(object.path), `Missing replicated object ${object.path}`);
}
const manifestBody = JSON.stringify(remoteManifest);
return {
replication: {
targetType: 's3',
targetPath: 'fake-target',
manifestPath: 'fake-target/manifest.json',
manifestSha256: crypto.createHash('sha256').update(manifestBody).digest('hex'),
objectCount: remoteManifest.objects.length,
totalSize: remoteManifest.totalSize,
completedAt: Date.now(),
},
};
}
if (methodArg === 'getBackupArchiveManifest') {
return { manifest: remoteManifest };
}
if (methodArg === 'downloadBackupArchiveObject') {
const remoteObject = remoteObjects.get(payloadArg.object.path);
assert(remoteObject, `Remote object ${payloadArg.object.path} not found`);
return remoteObject;
}
throw new Error(`Unexpected fake Cloudly request ${methodArg}`);
},
}),
},
};
(coreflow.cloudlyConnector as any).cloudlyApiClient = fakeCloudlyApiClient;
(coreflow.cloudlyConnector as any).identity = {
name: 'replication-test-coreflow',
role: 'cluster',
type: 'machine',
userId: 'cluster-remote-replication',
expiresAt: Date.now() + 3600 * 1000,
jwt: '',
clusterId: 'cluster-remote-replication',
};
const backupResult = await coreflow.backupManager.executeServiceBackup({
backupId: 'remote-replication-smoke',
service,
replication: {
enabled: true,
},
tags: {
scenario: scenarioName,
},
});
assert(backupResult.replication?.objectCount === remoteObjects.size, `Unexpected replication result: ${JSON.stringify(backupResult.replication)}`);
assert(remoteObjects.size > 0, 'Remote replication did not upload archive objects');
await corestore.stop();
await rm(join(dataDir, 'volume-archive'), { recursive: true, force: true });
await writeFile(join(mountpoint, 'state.txt'), 'after local archive loss\n');
corestore = new CoreStore({
dataDir,
bindAddress: '127.0.0.1',
publicHost: '127.0.0.1',
controlPort,
s3Port,
dbPort,
apiToken,
volumePluginSocketPath: socketPath,
});
await corestore.start();
await coreflow.backupManager.executeServiceRestore({
backupId: 'remote-replication-smoke',
service,
snapshots: backupResult.snapshots,
clear: true,
replication: {
enabled: true,
},
});
assert(await readFile(join(mountpoint, 'state.txt'), 'utf8') === 'before remote backup\n', 'Remote archive restore did not restore volume data');
} finally {
if (previousControlUrl === undefined) {
delete process.env.CORESTORE_CONTROL_URL;
} else {
process.env.CORESTORE_CONTROL_URL = previousControlUrl;
}
if (previousApiToken === undefined) {
delete process.env.CORESTORE_API_TOKEN;
} else {
process.env.CORESTORE_API_TOKEN = previousApiToken;
}
await corestore.stop().catch((errorArg) => {
console.log(`[${scenarioName}] Failed to stop replication Corestore: ${(errorArg as Error).message}`);
});
}
};
const main = async () => {
try {
await mkdir(buildDir, { recursive: true });
assertCoreflowVolumeMounts();
await assertCorestoreVolumeDriver();
await assertCoreflowBackupOrchestration();
await assertCoreflowRemoteReplication();
console.log(`[${scenarioName}] PASS`);
} finally {
await rm(buildDir, { recursive: true, force: true });
+42 -10
View File
@@ -15,13 +15,36 @@ const delayFor = async (millisecondsArg: number) => {
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
};
const run = async (commandArg: string, argsArg: string[]) => {
const outputCommand = async (
commandArg: string,
argsArg: string[],
optionsArg: Pick<Deno.CommandOptions, 'stdout' | 'stderr'>,
timeoutMsArg = 30000,
) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMsArg);
try {
const command = new Deno.Command(commandArg, {
args: argsArg,
signal: controller.signal,
...optionsArg,
});
return await command.output();
} catch (error) {
if ((error as Error).name === 'AbortError') {
throw new Error(`${commandArg} ${argsArg.join(' ')} timed out after ${timeoutMsArg}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
};
const run = async (commandArg: string, argsArg: string[]) => {
const output = await outputCommand(commandArg, argsArg, {
stdout: 'piped',
stderr: 'piped',
});
const output = await command.output();
const stdout = new TextDecoder().decode(output.stdout).trim();
const stderr = new TextDecoder().decode(output.stderr).trim();
if (stdout) {
@@ -48,12 +71,11 @@ const waitFor = async (checkFunctionArg: () => boolean | Promise<boolean>, messa
};
const dockerServiceExists = async (serviceNameArg: string) => {
const command = new Deno.Command('docker', {
args: ['service', 'inspect', serviceNameArg],
const output = await outputCommand('docker', ['service', 'inspect', serviceNameArg], {
stdout: 'null',
stderr: 'null',
});
return (await command.output()).success;
}, 15000);
return output.success;
};
const removeDockerService = async (serviceNameArg: string) => {
@@ -71,12 +93,16 @@ const assertNoPreexistingOneboxIngress = async () => {
const waitForDockerServiceRunning = async (serviceNameArg: string) => {
await waitFor(async () => {
const command = new Deno.Command('docker', {
args: ['service', 'ps', serviceNameArg, '--format', '{{.CurrentState}}'],
const output = await outputCommand('docker', [
'service',
'ps',
serviceNameArg,
'--format',
'{{.CurrentState}}',
], {
stdout: 'piped',
stderr: 'null',
});
const output = await command.output();
}, 15000);
if (!output.success) {
return false;
}
@@ -205,4 +231,10 @@ const main = async () => {
}
};
try {
await main();
Deno.exit(0);
} catch (error) {
console.error(error);
Deno.exit(1);
}
+44 -14
View File
@@ -14,13 +14,36 @@ const delayFor = async (millisecondsArg: number) => {
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
};
const run = async (commandArg: string, argsArg: string[]) => {
const outputCommand = async (
commandArg: string,
argsArg: string[],
optionsArg: Pick<Deno.CommandOptions, 'stdout' | 'stderr'>,
timeoutMsArg = 30000,
) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMsArg);
try {
const command = new Deno.Command(commandArg, {
args: argsArg,
signal: controller.signal,
...optionsArg,
});
return await command.output();
} catch (error) {
if ((error as Error).name === 'AbortError') {
throw new Error(`${commandArg} ${argsArg.join(' ')} timed out after ${timeoutMsArg}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
};
const run = async (commandArg: string, argsArg: string[]) => {
const output = await outputCommand(commandArg, argsArg, {
stdout: 'piped',
stderr: 'piped',
});
const output = await command.output();
const stdout = new TextDecoder().decode(output.stdout).trim();
const stderr = new TextDecoder().decode(output.stderr).trim();
if (stdout) {
@@ -47,12 +70,11 @@ const waitFor = async (checkFunctionArg: () => boolean | Promise<boolean>, messa
};
const dockerServiceExists = async (serviceNameArg: string) => {
const command = new Deno.Command('docker', {
args: ['service', 'inspect', serviceNameArg],
const output = await outputCommand('docker', ['service', 'inspect', serviceNameArg], {
stdout: 'null',
stderr: 'null',
});
return (await command.output()).success;
}, 15000);
return output.success;
};
const removeDockerService = async (serviceNameArg: string) => {
@@ -70,12 +92,16 @@ const assertNoPreexistingOneboxIngress = async () => {
const waitForDockerServiceRunning = async (serviceNameArg: string) => {
await waitFor(async () => {
const command = new Deno.Command('docker', {
args: ['service', 'ps', serviceNameArg, '--format', '{{.CurrentState}}'],
const output = await outputCommand('docker', [
'service',
'ps',
serviceNameArg,
'--format',
'{{.CurrentState}}',
], {
stdout: 'piped',
stderr: 'null',
});
const output = await command.output();
}, 15000);
if (!output.success) {
return false;
}
@@ -107,12 +133,10 @@ const requestRoute = async (protocolArg: 'http' | 'https', portArg: number) => {
curlArgs.unshift('-k');
}
const command = new Deno.Command('curl', {
args: curlArgs,
const output = await outputCommand('curl', curlArgs, {
stdout: 'piped',
stderr: 'piped',
});
const output = await command.output();
}, 15000);
const stdout = new TextDecoder().decode(output.stdout).trim();
const stderr = new TextDecoder().decode(output.stderr).trim();
if (!output.success) {
@@ -277,4 +301,10 @@ const main = async () => {
}
};
try {
await main();
Deno.exit(0);
} catch (error) {
console.error(error);
Deno.exit(1);
}
@@ -329,4 +329,10 @@ const main = async () => {
}
};
try {
await main();
Deno.exit(0);
} catch (error) {
console.error(error);
Deno.exit(1);
}
+28 -9
View File
@@ -4,8 +4,7 @@ import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
import { SmartNetwork } from '@push.rocks/smartnetwork';
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import * as cloudlyApiClient from '../../../api/ts/index.js';
import { Cloudly, type ICloudlyConfig } from '../../../cloudly/ts/index.js';
@@ -20,6 +19,13 @@ const buildDir = join(testingDir, '.nogit', 'registry-deploy-on-push', smokeId);
const coretrafficServiceName = `${smokeId}-coretraffic`;
const coreflowProxyServiceName = 'coreflow';
const stopFunctions: Array<() => Promise<void>> = [];
const tapNodeTools = new TapNodeTools({
registerCleanup: (cleanupFunctionArg) => {
stopFunctions.push(async () => {
await cleanupFunctionArg();
});
},
});
const delayFor = async (millisecondsArg: number) => {
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
@@ -367,15 +373,18 @@ const createCloudlyConfig = async (): Promise<ICloudlyConfig> => {
stopFunctions.push(async () => {
await smartmongo.stopAndDumpToDir(join(testingDir, '.nogit', 'mongodump', smokeId));
});
const smarts3 = await tapNodeTools.createSmarts3();
const smarts3 = await tapNodeTools.createSmartStorage();
stopFunctions.push(async () => {
await smarts3.stop();
});
const bucketName = `${smokeId}-bucket`;
await smarts3.createBucket(bucketName);
const smartnetwork = new SmartNetwork();
const publicPort = await smartnetwork.findFreePort(30000, 40000, { randomize: true });
const publicPort = await tapNodeTools.findFreePort({
startPort: 30000,
endPort: 40000,
randomize: true,
});
if (!publicPort) {
throw new Error('Could not find a free Cloudly scenario port');
}
@@ -386,7 +395,7 @@ const createCloudlyConfig = async (): Promise<ICloudlyConfig> => {
publicUrl: '127.0.0.1',
publicPort: String(publicPort),
mongoDescriptor: await smartmongo.getMongoDescriptor(),
s3Descriptor: await smarts3.getS3Descriptor({
s3Descriptor: await smarts3.getStorageDescriptor({
bucketName,
}),
sslMode: 'none',
@@ -533,8 +542,11 @@ const main = async () => {
createdCorechatNetwork = true;
}
const smartnetwork = new SmartNetwork();
const coretrafficHttpsPort = await smartnetwork.findFreePort(41000, 43000, { randomize: true });
const coretrafficHttpsPort = await tapNodeTools.findFreePort({
startPort: 41000,
endPort: 43000,
randomize: true,
});
if (!coretrafficHttpsPort) {
throw new Error('Could not find a free Coretraffic HTTPS test port');
}
@@ -547,7 +559,14 @@ const main = async () => {
coretrafficHttpsPort,
);
createdCoretrafficService = true;
await coreflow.cloudlyConnector.start();
const clusterIdentity = await testClient.getIdentityByToken(clusterToken, {
statefullIdentity: true,
tagConnection: true,
});
coreflow.cloudlyConnector.cloudlyApiClient = testClient as any;
coreflow.cloudlyConnector.identity = clusterIdentity;
coreflow.cloudlyConnector.coreflowJumpCode = clusterToken;
coreflow.cloudlyConnector.stop = async () => {};
console.log('[registry-deploy-on-push] Coreflow connector authenticated and tagged');
try {
await waitFor(async () => {