2026-05-08 16:24:45 +00:00
|
|
|
import assert from 'assert/strict';
|
2026-05-24 06:00:23 +00:00
|
|
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
2026-05-08 16:24:45 +00:00
|
|
|
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');
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-24 06:00:23 +00:00
|
|
|
const readWorkspaceJson = <T>(pathFromRoot: string) => {
|
|
|
|
|
return JSON.parse(readWorkspaceFile(pathFromRoot)) as T;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-08 16:24:45 +00:00
|
|
|
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}`);
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-24 06:00:23 +00:00
|
|
|
const assertPort = (valueArg: unknown, labelArg: string) => {
|
|
|
|
|
assert.ok(Number.isInteger(valueArg), `${labelArg} should be an integer`);
|
|
|
|
|
assert.ok((valueArg as number) >= 1 && (valueArg as number) <= 65535, `${labelArg} should be a valid TCP/UDP port`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const assertValidPublishedPort = (portArg: any, labelArg: string) => {
|
|
|
|
|
assertPort(portArg.targetPort, `${labelArg}.targetPort`);
|
|
|
|
|
const targetEnd = portArg.targetPortEnd ?? portArg.targetPort;
|
|
|
|
|
assertPort(targetEnd, `${labelArg}.targetPortEnd`);
|
|
|
|
|
assert.ok(targetEnd >= portArg.targetPort, `${labelArg} target range should be ascending`);
|
|
|
|
|
|
|
|
|
|
const publishedStart = portArg.publishedPort ?? portArg.targetPort;
|
|
|
|
|
const publishedEnd = portArg.publishedPortEnd ?? (publishedStart + (targetEnd - portArg.targetPort));
|
|
|
|
|
assertPort(publishedStart, `${labelArg}.publishedPort`);
|
|
|
|
|
assertPort(publishedEnd, `${labelArg}.publishedPortEnd`);
|
|
|
|
|
assert.ok(publishedEnd >= publishedStart, `${labelArg} published range should be ascending`);
|
|
|
|
|
assert.equal(targetEnd - portArg.targetPort, publishedEnd - publishedStart, `${labelArg} target and published ranges should have the same size`);
|
|
|
|
|
assert.ok(['tcp', 'udp'].includes(portArg.protocol ?? 'tcp'), `${labelArg}.protocol should be tcp or udp`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const assertValidAppCatalog = () => {
|
|
|
|
|
const catalog = readWorkspaceJson<{
|
|
|
|
|
schemaVersion: number;
|
|
|
|
|
apps: Array<{ id: string; latestVersion: string }>;
|
|
|
|
|
}>('appstore-apptemplates/catalog.json');
|
|
|
|
|
assert.equal(catalog.schemaVersion, 1, 'App catalog schema version should be 1');
|
|
|
|
|
|
|
|
|
|
const appIds = catalog.apps.map((appArg) => appArg.id);
|
|
|
|
|
assert.equal(new Set(appIds).size, appIds.length, 'App catalog ids should be unique');
|
|
|
|
|
|
|
|
|
|
const appDirs = readdirSync(resolve(root, 'appstore-apptemplates/apps'), { withFileTypes: true })
|
|
|
|
|
.filter((direntArg) => direntArg.isDirectory())
|
|
|
|
|
.map((direntArg) => direntArg.name)
|
|
|
|
|
.sort();
|
|
|
|
|
assert.deepEqual([...appIds].sort(), appDirs, 'Every app directory should be represented in catalog.json');
|
|
|
|
|
|
|
|
|
|
for (const catalogApp of catalog.apps) {
|
|
|
|
|
const appPath = `appstore-apptemplates/apps/${catalogApp.id}`;
|
|
|
|
|
const metaPath = `${appPath}/app.json`;
|
|
|
|
|
assert.ok(existsSync(resolve(root, metaPath)), `${metaPath} should exist`);
|
|
|
|
|
const appMeta = readWorkspaceJson<{
|
|
|
|
|
id: string;
|
|
|
|
|
latestVersion: string;
|
|
|
|
|
versions: string[];
|
|
|
|
|
}>(metaPath);
|
|
|
|
|
assert.equal(appMeta.id, catalogApp.id, `${metaPath} id should match its directory and catalog entry`);
|
|
|
|
|
assert.equal(appMeta.latestVersion, catalogApp.latestVersion, `${metaPath} latestVersion should match catalog.json`);
|
|
|
|
|
assert.ok(appMeta.versions.includes(appMeta.latestVersion), `${metaPath} versions should include latestVersion`);
|
|
|
|
|
|
|
|
|
|
const configPath = `${appPath}/versions/${appMeta.latestVersion}/config.json`;
|
|
|
|
|
assert.ok(existsSync(resolve(root, configPath)), `${configPath} should exist`);
|
|
|
|
|
const config = readWorkspaceJson<{
|
|
|
|
|
image: string;
|
|
|
|
|
port: number;
|
|
|
|
|
envVars?: Array<{ key: string; value?: string; description: string; required?: boolean }>;
|
|
|
|
|
volumes?: Array<string | { mountPath: string }>;
|
|
|
|
|
publishedPorts?: any[];
|
|
|
|
|
}>(configPath);
|
|
|
|
|
assert.ok(config.image, `${configPath} should define an image`);
|
|
|
|
|
assertPort(config.port, `${configPath}.port`);
|
|
|
|
|
|
|
|
|
|
const envKeys = new Set<string>();
|
|
|
|
|
for (const envVar of config.envVars || []) {
|
|
|
|
|
assert.ok(envVar.key, `${configPath} env var should define a key`);
|
|
|
|
|
assert.ok(!envKeys.has(envVar.key), `${configPath} env var ${envVar.key} should be unique`);
|
|
|
|
|
envKeys.add(envVar.key);
|
|
|
|
|
assert.equal(typeof envVar.description, 'string', `${configPath} env var ${envVar.key} should describe its purpose`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [index, volume] of (config.volumes || []).entries()) {
|
|
|
|
|
const mountPath = typeof volume === 'string' ? volume : volume.mountPath;
|
|
|
|
|
assert.ok(mountPath?.startsWith('/'), `${configPath} volume ${index} should define an absolute mountPath`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [index, publishedPort] of (config.publishedPorts || []).entries()) {
|
|
|
|
|
assertValidPublishedPort(publishedPort, `${configPath}.publishedPorts[${index}]`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-08 16:24:45 +00:00
|
|
|
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');
|
2026-05-24 06:00:23 +00:00
|
|
|
assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'installAppCatalogApp');
|
|
|
|
|
assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'externalImageRef');
|
|
|
|
|
assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'SERVICE_DOMAIN');
|
|
|
|
|
assertIncludes('cloudly/ts/manager.deployment/classes.deploymentmanager.ts', 'deploymentWorkspaceExec');
|
|
|
|
|
assertIncludes('coreflow/ts/coreflow.classes.coreflow.ts', 'deploymentRuntimeManager');
|
|
|
|
|
assertIncludes('coreflow/ts/coreflow.classes.deploymentruntime.ts', 'coreflowDeploymentWorkspaceExec');
|
|
|
|
|
assertIncludes('coreflow/ts/coreflow.classes.clustermanager.ts', 'externalImageRef');
|
|
|
|
|
assertIncludes('coreflow/ts/coreflow.classes.clustermanager.ts', 'resolveEnvTemplates');
|
|
|
|
|
assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/manifest'");
|
|
|
|
|
assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/object/read'");
|
|
|
|
|
assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/object/write'");
|
2026-05-08 16:24:45 +00:00
|
|
|
|
|
|
|
|
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 };');
|
2026-05-24 06:00:23 +00:00
|
|
|
assertIncludes('siprouter/ts/storage.ts', 'MONGODB_URI');
|
|
|
|
|
assertIncludes('siprouter/ts/storage.ts', 'S3_ENDPOINT');
|
|
|
|
|
assertIncludes('siprouter/Dockerfile', 'libjpeg-dev');
|
|
|
|
|
assertIncludes('siprouter/Dockerfile', 'libtiff-dev');
|
|
|
|
|
assertIncludes('siprouter/Dockerfile', 'libjpeg-turbo');
|
|
|
|
|
assertIncludes('siprouter/.smartconfig.json', 'linux/amd64');
|
|
|
|
|
assertIncludes('siprouter/.smartconfig.json', 'linux/arm64');
|
2026-05-08 16:24:45 +00:00
|
|
|
assertIncludes('onebox/ts/opsserver/classes.opsserver.ts', "'/v2/*'");
|
2026-05-24 06:00:23 +00:00
|
|
|
assertIncludes('onebox/ts/classes/appstore.ts', 'installApp(optionsArg: IAppInstallOptions)');
|
|
|
|
|
assertIncludes('onebox/ts/classes/appstore.ts', 'publishedPorts: optionsArg.publishedPorts || config.publishedPorts');
|
|
|
|
|
assertIncludes('onebox/ts/classes/docker.ts', 'expandPublishedPorts');
|
|
|
|
|
assertIncludes('onebox/ts/classes/docker.ts', 'Mounts: this.getSwarmVolumeMounts(service)');
|
|
|
|
|
assertIncludes('onebox/ts/database/migrations/migration-016-service-volumes.ts', 'volumes TEXT');
|
|
|
|
|
assertIncludes('onebox/ts/database/migrations/migration-017-service-published-ports.ts', 'published_ports TEXT');
|
|
|
|
|
assertIncludes('onebox/ts_interfaces/requests/appstore.ts', "method: 'installAppTemplate'");
|
|
|
|
|
assertIncludes('onebox/ts_interfaces/requests/appstore.ts', 'publishedPorts?: data.IServicePublishedPort[]');
|
|
|
|
|
assertIncludes('onebox/ts/classes/managed-dcrouter.ts', 'DCROUTER_ADMIN_API_TOKEN');
|
|
|
|
|
assertIncludes('api/ts/classes.cloudlyapiclient.ts', 'createNodeJumpCommand');
|
|
|
|
|
assertIncludes('interfaces/ts/appcatalog/types.ts', 'publishedPorts?: IAppCatalogPublishedPort[]');
|
2026-05-08 16:24:45 +00:00
|
|
|
assertIncludes('spark/ts/spark.cli.ts', 'CLOUDLY_URL: cloudlyUrl');
|
|
|
|
|
|
2026-05-24 06:00:23 +00:00
|
|
|
assertValidAppCatalog();
|
|
|
|
|
|
2026-05-08 16:24:45 +00:00
|
|
|
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');
|
|
|
|
|
|
2026-05-24 06:00:23 +00:00
|
|
|
const siprouterConfig = readWorkspaceJson<any>('appstore-apptemplates/apps/siprouter/versions/1.0.0/config.json');
|
|
|
|
|
assert.equal(siprouterConfig.platformRequirements?.mongodb, true);
|
|
|
|
|
assert.equal(siprouterConfig.platformRequirements?.s3, true);
|
|
|
|
|
assert.ok(siprouterConfig.publishedPorts.some((portArg: any) => portArg.targetPort === 20000 && portArg.targetPortEnd === 20200 && portArg.protocol === 'udp'));
|
|
|
|
|
|
|
|
|
|
const gitopsConfig = readWorkspaceJson<any>('appstore-apptemplates/apps/gitops/versions/1.0.0/config.json');
|
|
|
|
|
assert.ok(gitopsConfig.volumes.includes('/data/.serve.zone/gitops'));
|
|
|
|
|
|
2026-05-08 16:24:45 +00:00
|
|
|
console.log('codebase regression checks passed');
|