GitNexus/.devcontainer/profiles.test.cjs
Gergő Magyar b961e695ca
Some checks failed
Devcontainer Smoke / Config-transform unit tests (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (default) (push) Has been cancelled
Publish Development Images / publish (core) (push) Has been cancelled
Publish Development Images / publish (ui) (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (ui) (push) Has been cancelled
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / ci (push) Blocked by required conditions
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
fix(devcontainer): refresh Node and streamline development (#3610)
2026-10-17 21:05:13 +00:00

286 lines
12 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync, spawnSync } = require('node:child_process');
const ts = require('typescript');
const { profile } = require('./profile.cjs');
const { bootstrap } = require('./bootstrap-native.cjs');
const source = fs.readFileSync(path.join(__dirname, 'devcontainer.json'), 'utf8');
const parse = (text) => {
const result = ts.parseConfigFileTextToJson('devcontainer.json', text);
assert.equal(result.error, undefined);
return result.config;
};
test('native/UI profile inherits hooks, storage and defaults while changing workspace and browser setup', () => {
const c = parse(
profile(source, { native: true, ui: true, playwright: '1.63.0', ref: 'feature/test' }),
);
assert.match(c.workspaceMount, /type=volume/);
assert.match(c.onCreateCommand, /bootstrap-native/);
assert.equal(c.containerEnv.GITNEXUS_CLONE_REF, 'feature/test');
assert.equal(c.build.args.PLAYWRIGHT_VERSION, '1.63.0');
assert.equal(c.containerEnv.GITNEXUS_DEV_UI, '1');
assert.ok(c.mounts.some((m) => m.includes('ms-playwright')));
assert.ok(c.mounts.some((m) => m.includes('/home/node/.gitnexus')));
assert.deepEqual(c.capAdd, ['SYS_ADMIN']);
assert.equal(c.updateContentCommand, 'bash .devcontainer/install-deps.sh');
});
test('image consumers retain configured features and runtime lifecycle', () => {
for (const options of [{}, { native: true }, { ui: true }, { native: true, ui: true }]) {
const image = 'ghcr.io/example/devcontainer@sha256:abcd';
const c = parse(profile(source, { ...options, image }));
assert.equal(c.build, undefined);
assert.equal(c.image, image);
assert.deepEqual(c.features, parse(source).features);
assert.ok(c.features['ghcr.io/devcontainers/features/github-cli:1']);
assert.equal(c.postCreateCommand, 'bash .devcontainer/post-create.sh');
}
});
test('image selection accepts short names, uppercase tags, ports and digests', () => {
for (const image of [
'a',
'a:RC',
'ghcr.io/example/image:ReleaseCandidate',
'localhost:5000/example/image:ReleaseCandidate',
`ghcr.io/example/image@sha256:${'a'.repeat(64)}`,
`ghcr.io/example/image:RC@sha256:${'a'.repeat(64)}`,
]) {
assert.equal(parse(profile(source, { image })).image, image);
}
for (const image of [
'--privileged',
'image;bad',
'image name',
'image\nname',
'$(id)',
'"image"',
])
assert.throws(() => profile(source, { image }), /Invalid image reference/);
});
test('published toolchain metadata excludes host mounts and lifecycle hooks', () => {
const c = parse(profile(source, { imageOnly: true, ui: true, playwright: '1.63.0' }));
assert.equal(c.mounts, undefined);
assert.equal(c.updateContentCommand, undefined);
assert.equal(c.containerEnv, undefined);
assert.equal(c.build.args.INSTALL_BROWSER_DEPS, 'true');
assert.ok(c.features['ghcr.io/devcontainers/features/github-cli:1']);
});
test('profile generator rejects unsafe values and changed canonical layout', () => {
assert.throws(() => profile(source, { ref: '--upload-pack=bad' }), /Invalid/);
assert.throws(() => profile(source, { image: 'image;bad' }), /Invalid/);
assert.throws(
() => profile(source, { imageOnly: true, image: 'ghcr.io/example/image' }),
/--image cannot/,
);
for (const url of ['--upload-pack=bad', 'ext::command', 'https://token@github.com/user/repo.git'])
assert.throws(() => profile(source, { native: true, url }), /Invalid clone URL/);
assert.throws(
() => profile(source.replace('"context": "."', '"context": "elsewhere"')),
/layout changed/,
);
});
test('native branch validation rejects invalid Git refs before generation', () => {
for (const ref of [
'foo..',
'topic/.hidden',
'topic/name.lock',
'topic/name.lock/child',
'topic//child',
'topic/name.',
'topic/',
'HEAD',
]) {
assert.notEqual(spawnSync('git', ['check-ref-format', '--branch', ref]).status, 0);
assert.throws(() => profile(source, { native: true, ref }), /Invalid clone branch/);
}
for (const ref of [
'main',
'feature/fix-123',
'release/v1.2.3',
'topic/name.locked',
'topic/name.LOCK',
]) {
assert.equal(spawnSync('git', ['check-ref-format', '--branch', ref]).status, 0);
assert.equal(
parse(profile(source, { native: true, ref })).containerEnv.GITNEXUS_CLONE_REF,
ref,
);
}
});
test('UI variants preserve empty, custom, and absent capability selections', () => {
for (const capabilities of [[], ['SYS_PTRACE'], undefined]) {
const customized = source.replace(
' "capAdd": ["SYS_ADMIN"],',
capabilities === undefined ? '' : ` "capAdd": ${JSON.stringify(capabilities)},`,
);
for (const options of [{}, { native: true }, { image: 'ghcr.io/example/devcontainer:ui' }]) {
const c = parse(profile(customized, { ...options, ui: true, playwright: '1.63.0' }));
assert.deepEqual(c.capAdd, capabilities);
assert.deepEqual(c.runArgs, ['--shm-size=1g']);
}
}
});
test('native profiles accept fork remotes over HTTPS and Git SSH', () => {
for (const url of [
'https://github.com/contributor/GitNexus.git',
'git@github.com:contributor/GitNexus.git',
'ssh://git@example.com:2222/contributor/GitNexus.git',
]) {
const c = parse(profile(source, { native: true, url, ref: 'feature/fork-only' }));
assert.equal(c.containerEnv.GITNEXUS_CLONE_URL, url);
assert.equal(c.containerEnv.GITNEXUS_CLONE_REF, 'feature/fork-only');
}
});
test('profile CLI validates combinations before writing and generates fork configuration', (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-profile-cli-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const config = path.join(root, '.devcontainer');
fs.mkdirSync(config);
for (const file of ['profile.cjs', 'devcontainer.json', 'devcontainer-lock.json'])
fs.copyFileSync(path.join(__dirname, file), path.join(config, file));
fs.mkdirSync(path.join(root, 'gitnexus-web'));
fs.writeFileSync(
path.join(root, 'gitnexus-web/package-lock.json'),
JSON.stringify({
packages: { 'node_modules/@playwright/test': { version: '1.63.0' } },
}),
);
const run = (...args) =>
spawnSync(process.execPath, [path.join(config, 'profile.cjs'), ...args], {
cwd: root,
encoding: 'utf8',
});
for (const [args, error] of [
[
['image-build', '--image', 'ghcr.io/example/image'],
/--image cannot be used with image-build/,
],
[['native', '--url'], /--url requires a value/],
[['native', '--ref', '--ui'], /--ref requires a value/],
[['prebuilt', '--url', 'https://github.com/user/repo.git'], /require the native profile/],
[['native', '--ref', 'main', '--ref', 'other'], /Duplicate profile argument/],
[['native', '--ref', 'foo..'], /Invalid clone branch/],
]) {
const result = run(...args);
assert.notEqual(result.status, 0);
assert.match(result.stderr, error);
assert.equal(fs.existsSync(path.join(config, 'generated')), false);
}
const result = run(
'native',
'--url',
'https://github.com/contributor/GitNexus.git',
'--ref',
'feature/fork-only',
);
assert.equal(result.status, 0, result.stderr);
const c = parse(fs.readFileSync(path.join(config, 'generated/devcontainer.json'), 'utf8'));
assert.equal(c.containerEnv.GITNEXUS_CLONE_URL, 'https://github.com/contributor/GitNexus.git');
assert.equal(c.containerEnv.GITNEXUS_CLONE_REF, 'feature/fork-only');
for (const args of [
['prebuilt'],
['prebuilt', '--ui'],
['native', '--image', 'a'],
['ui', '--image', 'ghcr.io/example/image:ReleaseCandidate'],
]) {
const result = run(...args);
assert.equal(result.status, 0, result.stderr);
const generated = parse(
fs.readFileSync(path.join(config, 'generated/devcontainer.json'), 'utf8'),
);
assert.deepEqual(generated.features, parse(source).features);
if (args.includes('--image')) assert.equal(generated.image, args.at(-1));
}
});
test('native workspace preserves edits, rejects a different origin and resumes interrupted fetches', (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-native-profile-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const origin = path.join(root, 'origin');
fs.mkdirSync(origin);
const git = (...args) => execFileSync('git', args, { cwd: origin, stdio: 'pipe' });
git('init', '-b', 'main');
git('config', 'user.name', 'Profile test');
git('config', 'user.email', 'profile@example.invalid');
git('config', 'commit.gpgsign', 'false');
git('config', 'core.hooksPath', '/dev/null');
fs.writeFileSync(path.join(origin, 'file'), 'source');
git('add', 'file');
git('commit', '-m', 'fixture');
const checkout = path.join(root, 'checkout');
fs.mkdirSync(path.join(checkout, 'node_modules'), { recursive: true });
const bin = path.join(root, 'bin');
fs.mkdirSync(bin);
const ownershipLog = path.join(root, 'ownership.json');
fs.writeFileSync(
path.join(bin, 'sudo'),
`#!${process.execPath}\nrequire('node:fs').writeFileSync(${JSON.stringify(ownershipLog)}, JSON.stringify(process.argv.slice(2)));\n`,
{ mode: 0o755 },
);
const result = spawnSync(
process.execPath,
[path.join(__dirname, 'bootstrap-native.cjs'), checkout],
{
encoding: 'utf8',
env: {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
GITNEXUS_CLONE_URL: origin,
GITNEXUS_CLONE_REF: 'main',
},
},
);
assert.equal(result.status, 0, result.stderr);
const ownership = JSON.parse(fs.readFileSync(ownershipLog, 'utf8'));
assert.deepEqual(ownership.slice(0, 4), ['-n', 'find', checkout, '-xdev']);
assert.ok(ownership.includes('-h'));
const checkoutGit = (...args) =>
execFileSync('git', args, { cwd: checkout, encoding: 'utf8' }).trim();
assert.equal(checkoutGit('rev-parse', '--abbrev-ref', '@{upstream}'), 'origin/main');
assert.match(checkoutGit('pull'), /Already up to date/);
fs.writeFileSync(path.join(checkout, 'file'), 'local edits');
bootstrap(checkout, origin, 'main');
assert.equal(fs.readFileSync(path.join(checkout, 'file'), 'utf8'), 'local edits');
assert.throws(() => bootstrap(checkout, path.join(root, 'other'), 'main'), /another origin/);
const retry = path.join(root, 'retry');
assert.throws(() => bootstrap(retry, origin, 'new-branch'));
git('branch', 'new-branch');
bootstrap(retry, origin, 'new-branch');
assert.equal(fs.readFileSync(path.join(retry, 'file'), 'utf8'), 'source');
assert.equal(
execFileSync('git', ['rev-parse', '--abbrev-ref', '@{upstream}'], {
cwd: retry,
encoding: 'utf8',
}).trim(),
'origin/new-branch',
);
const conflict = path.join(root, 'conflict');
fs.mkdirSync(conflict);
fs.writeFileSync(path.join(conflict, 'file'), 'pre-existing user file');
assert.throws(() => bootstrap(conflict, origin, 'main'));
assert.equal(fs.readFileSync(path.join(conflict, 'file'), 'utf8'), 'pre-existing user file');
const ignored = path.join(root, 'ignored-conflict');
fs.mkdirSync(ignored);
execFileSync('git', ['init', '-q'], { cwd: ignored });
fs.writeFileSync(path.join(ignored, '.git/info/exclude'), 'file\n');
fs.writeFileSync(path.join(ignored, 'file'), 'ignored user file');
assert.throws(() => bootstrap(ignored, origin, 'main'));
assert.equal(fs.readFileSync(path.join(ignored, 'file'), 'utf8'), 'ignored user file');
// Preserve the user's file elsewhere, then verify the same checkout can resume.
fs.renameSync(path.join(ignored, 'file'), path.join(ignored, 'saved-user-file'));
bootstrap(ignored, origin, 'main');
assert.equal(fs.readFileSync(path.join(ignored, 'file'), 'utf8'), 'source');
assert.equal(fs.readFileSync(path.join(ignored, 'saved-user-file'), 'utf8'), 'ignored user file');
});