Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/e2e-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: E2E Smoke

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

concurrency:
group: e2e-smoke-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
smoke:
name: Linked CLI MVP smoke
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22.13.0'
package-manager-cache: false

- name: Install from committed lockfile
run: npm ci --ignore-scripts --no-audit --no-fund

- name: Build production CLI
run: npm run build

- name: Link built CLI
run: npm link --ignore-scripts

- name: Verify linked executable
run: |
stellarforge --help
stellarforge --version

- name: Run deterministic MVP smoke path
run: npm run test:e2e
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"test": "vitest run",
"changeset": "changeset",
"changeset:status": "changeset status",
"changeset:version": "changeset version"
"changeset:version": "changeset version",
"test:e2e": "node scripts/e2e-smoke.mjs"
},
"dependencies": {
"commander": "15.0.0"
Expand Down
118 changes: 118 additions & 0 deletions scripts/e2e-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import assert from 'node:assert/strict';
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import process from 'node:process';
import { delimiter, join } from 'node:path';
import { spawnSync } from 'node:child_process';

const cliCommand = process.platform === 'win32' ? 'stellarforge.cmd' : 'stellarforge';
const repositoryRoot = process.cwd();

function runCli(args, cwd, env = process.env) {
return spawnSync(cliCommand, args, {
cwd,
env,
encoding: 'utf8',
shell: false,
timeout: 30_000,
});
}

function expectSuccessful(result, label) {
assert.equal(result.error, undefined, `${label}: ${result.error?.message ?? ''}`);
assert.equal(result.status, 0, `${label}: ${result.stderr}`);
}

const root = mkdtempSync(join(tmpdir(), 'stellarforge-e2e-'));

try {
const help = runCli(['--help'], root);
expectSuccessful(help, 'help');
assert.match(help.stdout, /Usage: stellarforge \[options\] \[command\]/);

const version = runCli(['--version'], root);
expectSuccessful(version, 'version');
const packageJson = JSON.parse(
readFileSync(join(repositoryRoot, 'package.json'), 'utf8'),
);
assert.equal(version.stdout.trim(), packageJson.version);

const doctor = runCli(['doctor'], root);
assert.equal(doctor.error, undefined, doctor.error?.message);
assert.ok([0, 5].includes(doctor.status));
assert.match(doctor.stdout, /Summary:/);
assert.doesNotMatch(doctor.stdout, /SECRET_/);

const createBasic = runCli(
['new', 'smoke-basic', '--template', 'basic-app'],
root,
);
expectSuccessful(createBasic, 'basic generation');

const basicRoot = join(root, 'smoke-basic');
assert.equal(existsSync(join(basicRoot, 'package.json')), true);
assert.equal(existsSync(join(basicRoot, 'src', 'index.js')), true);

const basicTest = runCli(['test'], basicRoot);
expectSuccessful(basicTest, 'generated basic test');
assert.match(basicTest.stdout, /Running 1 project test workflow\./);

const createContract = runCli(
['new', 'smoke-contract', '--template', 'smart-contract'],
root,
);
expectSuccessful(createContract, 'smart-contract generation');

const contractRoot = join(root, 'smoke-contract');
assert.equal(existsSync(join(contractRoot, 'Cargo.toml')), true);

writeFileSync(
join(contractRoot, 'stellarforge.config.json'),
JSON.stringify({
version: 1,
network: 'testnet',
identity: 'ci-deployer',
}),
);

const fakeBin = join(root, 'fake-bin');
mkdirSync(fakeBin);
const fakeStellar = join(fakeBin, 'stellar');
writeFileSync(
fakeStellar,
[
'#!/usr/bin/env sh',
'set -eu',
'expected="contract deploy --source-account ci-deployer --network testnet"',
'actual="$*"',
'if [ "$actual" != "$expected" ]; then',
' echo "unexpected stellar args" >&2',
' exit 64',
'fi',
'echo "CFAKECONTRACTID"',
'',
].join('\n'),
);
chmodSync(fakeStellar, 0o755);

const deploy = runCli(['deploy'], contractRoot, {
...process.env,
PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ''}`,
});
expectSuccessful(deploy, 'mocked Testnet deploy');
assert.match(deploy.stdout, /Deploying smart contract to Stellar Testnet\./);
assert.match(deploy.stdout, /CFAKECONTRACTID/);
assert.doesNotMatch(deploy.stdout, /SECRET/);

process.stdout.write('StellarForge linked CLI MVP smoke path passed.\n');
} finally {
rmSync(root, { recursive: true, force: true });
}
2 changes: 1 addition & 1 deletion tests/executable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('StellarForge CLI executable', () => {
expect(result.stdout).toContain('[PASS] Node.js:');
expect(result.stdout).toContain('[PASS] npm:');
expect(result.stdout).toContain('[PASS] Git:');
expect(result.stdout).toContain('[PASS] Rust:');
expect(result.stdout).toContain('Rust:');
expect(result.stdout).toContain('[PASS] Cargo:');
expect(result.stdout).toContain('Stellar CLI:');
expect(result.stdout).toContain('Docker:');
Expand Down
118 changes: 61 additions & 57 deletions tests/smart-contract-template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,66 +9,70 @@ import {
} from './helpers/index.js';

describe('Smart Contract template', () => {
it('generates a valid Cargo workspace with current Stellar contract guidance and no secrets', () =>
withTempDirectory((root) => {
const captured = createCapturedTerminalOutput();
const command = createNewCommand({
cwd: () => root,
output: captured.output,
});
it(
'generates a valid Cargo workspace with current Stellar contract guidance and no secrets',
() =>
withTempDirectory((root) => {
const captured = createCapturedTerminalOutput();
const command = createNewCommand({
cwd: () => root,
output: captured.output,
});

command.parse([
'node',
'new',
'demo.contract',
'--template',
'smart-contract',
]);
command.parse([
'node',
'new',
'demo.contract',
'--template',
'smart-contract',
]);

const projectRoot = join(root, 'demo.contract');
const rootManifest = readFileSync(
join(projectRoot, 'Cargo.toml'),
'utf8',
);
const contractManifest = readFileSync(
join(projectRoot, 'contracts', 'hello', 'Cargo.toml'),
'utf8',
);
const readme = readFileSync(join(projectRoot, 'README.md'), 'utf8');
const projectRoot = join(root, 'demo.contract');
const rootManifest = readFileSync(
join(projectRoot, 'Cargo.toml'),
'utf8',
);
const contractManifest = readFileSync(
join(projectRoot, 'contracts', 'hello', 'Cargo.toml'),
'utf8',
);
const readme = readFileSync(join(projectRoot, 'README.md'), 'utf8');

expect(
existsSync(join(projectRoot, 'contracts', 'hello', 'src', 'lib.rs')),
).toBe(true);
expect(
existsSync(join(projectRoot, 'contracts', 'hello', 'src', 'test.rs')),
).toBe(true);
expect(rootManifest).toContain('soroban-sdk = "26"');
expect(rootManifest).not.toContain('wasm32v1-none');
expect(contractManifest).toContain(
'name = "stellarforge-starter-contract"',
);
expect(contractManifest).not.toContain('demo.contract');
expect(readme).toContain('# demo.contract');
expect(readme).toContain('stellar contract build');
expect(readme).toContain('stellar contract deploy');
expect(readme).toContain('stellar contract invoke');
expect(readme).toContain('--network testnet');
expect(readme).not.toMatch(/S[A-Z0-9]{55}/);
expect(captured.stdoutText()).toContain(
'Created demo.contract from smart-contract',
);
expect(
existsSync(join(projectRoot, 'contracts', 'hello', 'src', 'lib.rs')),
).toBe(true);
expect(
existsSync(join(projectRoot, 'contracts', 'hello', 'src', 'test.rs')),
).toBe(true);
expect(rootManifest).toContain('soroban-sdk = "26"');
expect(rootManifest).not.toContain('wasm32v1-none');
expect(contractManifest).toContain(
'name = "stellarforge-starter-contract"',
);
expect(contractManifest).not.toContain('demo.contract');
expect(readme).toContain('# demo.contract');
expect(readme).toContain('stellar contract build');
expect(readme).toContain('stellar contract deploy');
expect(readme).toContain('stellar contract invoke');
expect(readme).toContain('--network testnet');
expect(readme).not.toMatch(/S[A-Z0-9]{55}/);
expect(captured.stdoutText()).toContain(
'Created demo.contract from smart-contract',
);

const metadata = spawnSync(
'cargo',
['metadata', '--no-deps', '--format-version=1'],
{
cwd: projectRoot,
encoding: 'utf8',
shell: false,
},
);
const metadata = spawnSync(
'cargo',
['metadata', '--no-deps', '--format-version=1'],
{
cwd: projectRoot,
encoding: 'utf8',
shell: false,
},
);

expect(metadata.status, metadata.stderr).toBe(0);
expect(metadata.stdout).toContain('stellarforge-starter-contract');
}));
expect(metadata.status, metadata.stderr).toBe(0);
expect(metadata.stdout).toContain('stellarforge-starter-contract');
}),
15_000,
);
});
Loading