Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple documentation files #364

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
.idea
.vscode

node_modules
dist
__pycache__
*.egg-info
*coverage*
*.tar.gz
*.tgz
*.tgz
191 changes: 165 additions & 26 deletions cli/commands/publish/upload.manifest.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ethers, Wallet } from "ethers";
import { keccak256 } from "../../utils";
import {ethers, Wallet} from "ethers";
import {keccak256} from "../../utils";
import provideUploadManifest, {
ChainSettings,
DocumentationSetting,
UploadManifest,
} from "./upload.manifest";

describe("uploadManifest", () => {
let uploadManifest: UploadManifest;
const mockSystemTime = new Date();
const mockFilesystem = {
existsSync: jest.fn(),
readFileSync: jest.fn(),
Expand All @@ -19,7 +20,11 @@ describe("uploadManifest", () => {
const mockLongDescription = "some long description";
const mockAgentId = "0xagentId";
const mockVersion = "0.1";
const mockDocumentation = "README.md";
const mockReadmeFilePath = "README.md";
const mockDocumentationSettings: DocumentationSetting[] = [
{title: 'General', filePath: 'General.md'},
{title: 'API Guide', filePath: 'API.md'},
];
const mockRepository = "github.com/myrepository";
const mockLicenseUrl = "github.com/myrepository";
const mockPromoUrl = "github.com/myrepository";
Expand Down Expand Up @@ -60,9 +65,11 @@ describe("uploadManifest", () => {
const resetMocks = () => {
mockFilesystem.existsSync.mockReset();
mockFilesystem.statSync.mockReset();
mockFilesystem.readFileSync.mockReset();
mockAddToIpfs.mockReset();
};

beforeAll(() => {
const setupUploadManifest = (props?: { documentationSettings?: DocumentationSetting[] }) => {
uploadManifest = provideUploadManifest(
mockFilesystem,
mockAddToIpfs,
Expand All @@ -72,7 +79,8 @@ describe("uploadManifest", () => {
mockLongDescription,
mockAgentId,
mockVersion,
mockDocumentation,
mockReadmeFilePath,
props?.documentationSettings,
mockRepository,
mockLicenseUrl,
mockPromoUrl,
Expand All @@ -81,54 +89,144 @@ describe("uploadManifest", () => {
mockExternal,
mockChainSettings
);
});
}

beforeEach(() => {
jest.useFakeTimers().setSystemTime(mockSystemTime);
resetMocks();
});

it("throws error if documentation not found", async () => {
afterEach(() => {
jest.useRealTimers();
})

it("throws error if readme file not found and documentation settings undefined", async () => {
mockFilesystem.existsSync.mockReturnValueOnce(false);

try {
setupUploadManifest();
await uploadManifest(mockImageRef, mockPrivateKey);
} catch (e) {
expect(e.message).toBe(
`documentation file ${mockDocumentation} not found`
`documentation file ${mockReadmeFilePath} not found`
);
}

expect(mockFilesystem.existsSync).toHaveBeenCalledTimes(1);
expect(mockFilesystem.existsSync).toHaveBeenCalledWith(mockDocumentation);
expect(mockFilesystem.existsSync).toHaveBeenCalledWith(mockReadmeFilePath);
});

it("throws error if documentation file is empty", async () => {
it("throws error if readme file is empty and documentation settings undefined", async () => {
mockFilesystem.existsSync.mockReturnValueOnce(true);
mockFilesystem.statSync.mockReturnValueOnce({ size: 0 });
mockFilesystem.statSync.mockReturnValueOnce({size: 0});

try {
setupUploadManifest();
await uploadManifest(mockImageRef, mockPrivateKey);
} catch (e) {
expect(e.message).toBe(
`documentation file ${mockDocumentation} cannot be empty`
`documentation file ${mockReadmeFilePath} cannot be empty`
);
}

expect(mockFilesystem.existsSync).toHaveBeenCalledTimes(1);
expect(mockFilesystem.existsSync).toHaveBeenCalledWith(mockDocumentation);
expect(mockFilesystem.existsSync).toHaveBeenCalledWith(mockReadmeFilePath);
expect(mockFilesystem.statSync).toHaveBeenCalledTimes(1);
expect(mockFilesystem.statSync).toHaveBeenCalledWith(mockReadmeFilePath);
});

it("throws error if one of documentation files not found", async () => {
const missingFile = 'API.md';

mockFilesystem.existsSync.mockImplementation((fileName: string) => fileName !== missingFile);
mockFilesystem.statSync.mockImplementation((fileName: string) => {
if (fileName === missingFile) throw new Error(`Cannot find ${fileName}`);
return {size: 1};
});

try {
setupUploadManifest({documentationSettings: mockDocumentationSettings});
await uploadManifest(mockImageRef, mockPrivateKey);
} catch (e) {
expect(e.message).toBe(
`documentation file ${missingFile} not found`
);
}

expect(mockFilesystem.existsSync).toHaveBeenCalledTimes(2);
expect(mockFilesystem.statSync).toHaveBeenCalledTimes(1);
expect(mockFilesystem.statSync).toHaveBeenCalledWith(mockDocumentation);
});

it('throws error if documentation settings defined with empty array', async () => {
try {
setupUploadManifest({documentationSettings: []});
await uploadManifest(mockImageRef, mockPrivateKey);
} catch (e) {
expect(e.message).toBe(
`documentationSettings must be non-empty array`
);
}

expect(mockFilesystem.existsSync).toHaveBeenCalledTimes(0);
expect(mockFilesystem.statSync).toHaveBeenCalledTimes(0);
})

it("throws error if one of documentation files is empty", async () => {
const emptyFile = 'API.md';

mockFilesystem.existsSync.mockReturnValue(true);
mockFilesystem.statSync.mockImplementation((fileName: string) => {
if (fileName === emptyFile) return {size: 0};
return {size: 1};
});

try {
setupUploadManifest({documentationSettings: mockDocumentationSettings});
await uploadManifest(mockImageRef, mockPrivateKey);
} catch (e) {
expect(e.message).toBe(
`documentation file ${emptyFile} cannot be empty`
);
}

expect(mockFilesystem.existsSync).toHaveBeenCalledTimes(2);
expect(mockFilesystem.statSync).toHaveBeenCalledTimes(2);
});

it("throws error if one of documentation settings has empty title", async () => {
mockFilesystem.existsSync.mockReturnValue(true);
mockFilesystem.statSync.mockReturnValue({size: 1});

try {
setupUploadManifest({
documentationSettings: [{
title: ' ',
filePath: 'FILE.md'
}]
});
await uploadManifest(mockImageRef, mockPrivateKey);
} catch (e) {
expect(e.message).toBe(
`title in documentationSettings must be non-empty string`
);
}

expect(mockFilesystem.existsSync).toHaveBeenCalledTimes(0);
expect(mockFilesystem.statSync).toHaveBeenCalledTimes(0);
});

it("uploads signed manifest to ipfs and returns ipfs reference", async () => {
const systemTime = new Date();
jest.useFakeTimers().setSystemTime(systemTime);
setupUploadManifest();

mockFilesystem.existsSync.mockReturnValueOnce(true);
mockFilesystem.statSync.mockReturnValueOnce({ size: 1 });
const mockDocumentationFile = JSON.stringify({ some: "documentation" });
mockFilesystem.statSync.mockReturnValueOnce({size: 1});

const mockDocumentationFile = JSON.stringify({some: "documentation"});
mockFilesystem.readFileSync.mockReturnValueOnce(mockDocumentationFile);

const mockDocumentationRef = "docRef";
mockAddToIpfs.mockReturnValueOnce(mockDocumentationRef);

const mockManifest = {
from: new Wallet(mockPrivateKey).address,
name: mockAgentDisplayName,
Expand All @@ -137,9 +235,12 @@ describe("uploadManifest", () => {
agentId: mockAgentName,
agentIdHash: mockAgentId,
version: mockVersion,
timestamp: systemTime.toUTCString(),
timestamp: mockSystemTime.toUTCString(),
imageReference: mockImageRef,
documentation: mockDocumentationRef,
documentation: JSON.stringify([{
title: 'README',
ipfsUrl: mockDocumentationRef
}]),
repository: mockRepository,
licenseUrl: mockLicenseUrl,
promoUrl: mockPromoUrl,
Expand All @@ -155,12 +256,12 @@ describe("uploadManifest", () => {

expect(manifestRef).toBe(mockManifestRef);
expect(mockFilesystem.existsSync).toHaveBeenCalledTimes(1);
expect(mockFilesystem.existsSync).toHaveBeenCalledWith(mockDocumentation);
expect(mockFilesystem.existsSync).toHaveBeenCalledWith(mockReadmeFilePath);
expect(mockFilesystem.statSync).toHaveBeenCalledTimes(1);
expect(mockFilesystem.statSync).toHaveBeenCalledWith(mockDocumentation);
expect(mockFilesystem.statSync).toHaveBeenCalledWith(mockReadmeFilePath);
expect(mockFilesystem.readFileSync).toHaveBeenCalledTimes(1);
expect(mockFilesystem.readFileSync).toHaveBeenCalledWith(
mockDocumentation,
mockReadmeFilePath,
"utf8"
);
expect(mockAddToIpfs).toHaveBeenCalledTimes(2);
Expand All @@ -171,8 +272,46 @@ describe("uploadManifest", () => {
);
expect(mockAddToIpfs).toHaveBeenNthCalledWith(
2,
JSON.stringify({ manifest: mockManifest, signature })
JSON.stringify({manifest: mockManifest, signature})
);
jest.useRealTimers();
});

it("uploads documentation settings correctly", async () => {
setupUploadManifest({ documentationSettings: mockDocumentationSettings });

// mocking file existence and content
mockFilesystem.existsSync.mockReturnValue(true);
mockFilesystem.statSync.mockReturnValue({ size: 1 });
mockFilesystem.readFileSync.mockImplementation((fileName: string) => {
if (fileName === 'General.md') return 'General Content';
if (fileName === 'API.md') return 'API Content';
});

// mocking IPFS addition
const mockGeneralDocRef = "generalDocRef";
const mockApiDocRef = "apiDocRef";
mockAddToIpfs.mockImplementation((content: string) => {
if (content === 'General Content') return mockGeneralDocRef;
if (content === 'API Content') return mockApiDocRef;
});

const manifestRef = await uploadManifest(mockImageRef, mockPrivateKey);

// asserting that files were read correctly
expect(mockFilesystem.existsSync).toHaveBeenCalledTimes(2);
expect(mockFilesystem.readFileSync).toHaveBeenCalledWith('General.md', 'utf8');
expect(mockFilesystem.readFileSync).toHaveBeenCalledWith('API.md', 'utf8');

// asserting that files were added to IPFS correctly
expect(mockAddToIpfs).toHaveBeenCalledTimes(3); // 2 docs + 1 manifest
expect(mockAddToIpfs).toHaveBeenCalledWith('General Content');
expect(mockAddToIpfs).toHaveBeenCalledWith('API Content');

// asserting manifest structure
const expectedDocumentation = JSON.stringify([
{ title: 'General', ipfsUrl: mockGeneralDocRef },
{ title: 'API Guide', ipfsUrl: mockApiDocRef },
]);
expect(JSON.parse(mockAddToIpfs.mock.calls[2][0]).manifest.documentation).toEqual(expectedDocumentation);
});
});
Loading
Loading