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
6 changes: 3 additions & 3 deletions src/diagnostics/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ const STELLAR_CLI_INSTALL_URL =

function parseStellarVersion(output: string): string | null {
const value = output.trim();
const match =
/^v?(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
return match?.[1] ?? null;
// prettier-ignore
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value);
return match ? match[1] + '.' + match[2] + '.' + match[3] : null;
}

export function createStellarDiagnostic(
Expand Down
94 changes: 94 additions & 0 deletions tests/stellar-diagnostic-failure-matrix.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest';
import type { RunCommand } from '../src/diagnostics/process.js';
import { createStellarDiagnostic } from '../src/diagnostics/stellar.js';

interface CommandResultFixture {
readonly status: number | null;
readonly stdout?: string;
readonly stderr?: string;
readonly error?: Error;
}

function runDiagnostic(fixture: CommandResultFixture) {
const execute: RunCommand = () => ({
status: fixture.status,
stdout: fixture.stdout ?? '',
stderr: fixture.stderr ?? '',
...(fixture.error === undefined ? {} : { error: fixture.error }),
});

return createStellarDiagnostic(execute).run();
}

describe('Stellar CLI diagnostic failure matrix', () => {
it.each([
['27.0.9', '27.0.9'],
['26.0.0', '26.0.0'],
['20.0.0', '20.0.0'],
['0.1.0', '0.1.0'],
['v27.0.1', '27.0.1'],
['27.0.0-rc.1', '27.0.0'],
])('rejects supported-floor predecessor %s', (output, extractedVersion) => {
const diagnostic = runDiagnostic({ status: 0, stdout: output });

expect(diagnostic.status).toBe('fail');
expect(diagnostic.message).toBe(
`Unsupported Stellar CLI version ${extractedVersion}.`,
);
expect(diagnostic.remediation).toContain('>=27.1.0');
});

it.each([
'{"version":"27.1.0"}',
'version: 27.1.0',
'<html><body>502 Bad Gateway</body></html>',
])('rejects structured or proxy-error output %s', (output) => {
const diagnostic = runDiagnostic({ status: 0, stdout: output });

expect(diagnostic.status).toBe('fail');
expect(diagnostic.message).toBe(
'Stellar CLI returned an unrecognized version.',
);
});

it.each([
{ status: 2, stderr: 'unrecognized option' },
{ status: 126, stderr: 'permission denied' },
{ status: 137, stderr: 'process killed' },
{
status: 1,
stdout: '27.1.0',
stderr: 'panic: internal failure',
},
])('fails closed for non-zero execution result $status', (fixture) => {
const diagnostic = runDiagnostic(fixture);

expect(diagnostic.status).toBe('fail');
expect(diagnostic.message).toBe(
'Stellar CLI is unavailable or could not be executed.',
);
expect(diagnostic.remediation).toContain(
'https://developers.stellar.org/docs/tools/cli/install-cli',
);
});

it.each([
new Error('spawn stellar ENOENT SECRET_PASSPHRASE=do-not-leak'),
new Error('spawn stellar EACCES SENSITIVE_KEY=do-not-leak'),
])('does not expose executable errors or stderr details', (error) => {
const diagnostic = runDiagnostic({
status: null,
stderr: 'SENSITIVE_KEY=do-not-leak',
error,
});
const serialized = JSON.stringify(diagnostic);

expect(diagnostic.status).toBe('fail');
expect(diagnostic.message).toBe(
'Stellar CLI is unavailable or could not be executed.',
);
expect(serialized).not.toContain('SECRET_PASSPHRASE');
expect(serialized).not.toContain('SENSITIVE_KEY');
expect(serialized).not.toContain('do-not-leak');
});
});
63 changes: 63 additions & 0 deletions tests/stellar-diagnostic-semver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest';
import type { RunCommand } from '../src/diagnostics/process.js';
import { createStellarDiagnostic } from '../src/diagnostics/stellar.js';

function runVersion(output: string) {
const execute: RunCommand = () => ({
status: 0,
stdout: output,
stderr: '',
});

return createStellarDiagnostic(execute).run();
}

describe('Stellar CLI semantic-version grammar', () => {
it.each([
['27.1.0', '27.1.0'],
['v27.1.0', '27.1.0'],
['27.1.0-rc.1', '27.1.0'],
['27.1.0+build.42', '27.1.0'],
['27.1.0-rc.1+sha.abcdef', '27.1.0'],
['28.0.0-preview.3', '28.0.0'],
['27.10.25', '27.10.25'],
['100.0.0', '100.0.0'],
['27.1.0-0', '27.1.0'],
['27.1.0+001', '27.1.0'],
])('accepts valid SemVer output %s', (output, expectedVersion) => {
const diagnostic = runVersion(output);

expect(diagnostic.status).toBe('pass');
expect(diagnostic.message).toBe(
`Supported version ${expectedVersion} detected.`,
);
});

it.each([
'027.1.0',
'9007199254740992.0.0',
'27.01.0',
'27.1.00',
'27.1.0-',
'27.1.0+',
'27.1.0-..',
'27.1.0-alpha..beta',
'27.1.0+build..7',
'27.1.0-01',
'27.1.0-alpha.01',
'27.1.0-alpha+',
'27.1.0+build+extra',
'27.1.0a',
'27.1.0.0',
'27.1.0 (built 2026-01-01)',
'27.1.0 extra-tokens',
'vA.B.C',
])('rejects malformed SemVer output %s', (output) => {
const diagnostic = runVersion(output);

expect(diagnostic.status).toBe('fail');
expect(diagnostic.message).toBe(
'Stellar CLI returned an unrecognized version.',
);
});
});
Loading