@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
147 lines • 7.02 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { expect } from 'chai';
import { afterEach, beforeEach, describe, it } from 'mocha';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import sinon from 'sinon';
import { DiagnosticsReporter } from '../../../../src/commands/util/diagnostics-reporter.js';
import { ShellRunner } from '../../../../src/core/shell-runner.js';
import { SoloError } from '../../../../src/core/errors/solo-error.js';
function makeLoggerStub() {
return {
info: sinon.stub(),
warn: sinon.stub(),
error: sinon.stub(),
debug: sinon.stub(),
showUser: sinon.stub(),
showUserError: sinon.stub(),
showList: sinon.stub(),
showJSON: sinon.stub(),
addMessageGroup: sinon.stub(),
getMessageGroup: sinon.stub().returns([]),
addMessageGroupMessage: sinon.stub(),
showMessageGroup: sinon.stub(),
getMessageGroupKeys: sinon.stub().returns([]),
showAllMessageGroups: sinon.stub(),
setDevMode: sinon.stub(),
isDevMode: sinon.stub().returns(false),
nextTraceId: sinon.stub(),
prepMeta: sinon.stub().callsFake((meta) => meta ?? {}),
flush: sinon.stub().callsFake((callback) => callback()),
};
}
describe('DiagnosticsReporter', () => {
let temporaryDirectory;
let loggerStub;
beforeEach(() => {
temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'solo-diagnostics-reporter-'));
loggerStub = makeLoggerStub();
});
afterEach(() => {
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
sinon.restore();
});
describe('isGhCliAvailable', () => {
it('returns true when gh is on the PATH', async () => {
sinon.stub(ShellRunner.prototype, 'run').resolves(['/usr/bin/gh']);
expect(await DiagnosticsReporter.isGhCliAvailable(loggerStub)).to.equal(true);
});
it('returns false when gh is not found', async () => {
sinon.stub(ShellRunner.prototype, 'run').rejects(new Error('not found'));
expect(await DiagnosticsReporter.isGhCliAvailable(loggerStub)).to.equal(false);
});
});
describe('findLatestDebugZip', () => {
it('returns undefined when directory does not exist', () => {
expect(DiagnosticsReporter.findLatestDebugZip('/nonexistent-dir-xyz', 'test-deployment', Date.now())).to.equal(undefined);
});
it('returns undefined when no matching zip exists', () => {
expect(DiagnosticsReporter.findLatestDebugZip(temporaryDirectory, 'my-deployment', 0)).to.equal(undefined);
});
it('finds the most recently modified zip created after the start time', () => {
const deployment = 'my-deployment';
const beforeStart = Date.now() - 1000;
const zipPath = path.join(temporaryDirectory, `solo-debug-${deployment}-2026-04-01T10-00-00.zip`);
fs.writeFileSync(zipPath, 'fake zip content');
const result = DiagnosticsReporter.findLatestDebugZip(temporaryDirectory, deployment, beforeStart);
expect(result).to.equal(zipPath);
});
it('ignores zip files created before the start time', () => {
const deployment = 'my-deployment';
const zipPath = path.join(temporaryDirectory, `solo-debug-${deployment}-old.zip`);
fs.writeFileSync(zipPath, 'old fake content');
// Start time is in the future relative to the file's mtime
const result = DiagnosticsReporter.findLatestDebugZip(temporaryDirectory, deployment, Date.now() + 5000);
expect(result).to.equal(undefined);
});
});
describe('readAnalysisContent', () => {
it('returns empty string when the analysis file does not exist', () => {
expect(DiagnosticsReporter.readAnalysisContent(temporaryDirectory)).to.equal('');
});
it('returns the file content when the analysis file exists', () => {
const analysisPath = path.join(temporaryDirectory, 'diagnostics-analysis.txt');
fs.writeFileSync(analysisPath, 'some analysis content');
expect(DiagnosticsReporter.readAnalysisContent(temporaryDirectory)).to.equal('some analysis content');
});
});
describe('buildIssueBody', () => {
it('includes all required metadata fields', () => {
const body = DiagnosticsReporter.buildIssueBody({
soloVersion: '1.2.3',
deployment: 'my-deploy',
timestamp: '2026-04-01T10-00-00',
analysisDirectory: '/tmp/analysis',
zipFilePath: '/tmp/solo-debug.zip',
});
expect(body).to.include('Solo Version**: 1.2.3');
expect(body).to.include('Deployment**: my-deploy');
expect(body).to.include('2026-04-01T10-00-00');
expect(body).to.include('/tmp/solo-debug.zip');
expect(body).to.include('Please attach it to this issue');
});
it('uses (not specified) when deployment is empty', () => {
const body = DiagnosticsReporter.buildIssueBody({
soloVersion: '1.0.0',
deployment: '',
timestamp: '2026-04-01T10-00-00',
analysisDirectory: '/tmp/analysis',
});
expect(body).to.include('(not specified)');
});
});
describe('createGitHubIssue', () => {
let executeGhCommandStub;
const mockPid = 12_345;
beforeEach(() => {
executeGhCommandStub = sinon.stub(DiagnosticsReporter, 'executeGhCommand');
});
it('returns the issue URL on success', async () => {
const expectedUrl = 'https://github.com/hiero-ledger/solo/issues/42';
executeGhCommandStub.returns({
status: 0,
stdout: `${expectedUrl}\n`,
stderr: '',
output: [undefined, `${expectedUrl}\n`, ''],
pid: mockPid,
signal: undefined,
});
const url = await DiagnosticsReporter.createGitHubIssue(loggerStub, 'Test Title', 'Test Body', '/tmp/analysis');
expect(url).to.equal(expectedUrl);
expect(executeGhCommandStub).to.have.been.calledOnce;
});
it('throws SoloError when gh command fails', async () => {
executeGhCommandStub.returns({
status: 1,
stdout: '',
stderr: 'authentication required',
output: [undefined, '', 'authentication required'],
pid: mockPid,
signal: undefined,
});
await expect(DiagnosticsReporter.createGitHubIssue(loggerStub, 'Test Title', 'Test Body', '/tmp/analysis')).to.be.rejectedWith(SoloError, /Failed to create GitHub issue/);
});
});
});
//# sourceMappingURL=diagnostics-reporter.test.js.map