UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

457 lines (383 loc) • 15.5 kB
import pathUtil from 'path' import { buildPermissionContract, EventSignature, } from '@sprucelabs/mercury-types' import { buildEmitTargetAndPayloadSchema, eventContractUtil, eventTargetSchema, } from '@sprucelabs/spruce-event-utils' import { diskUtil, MERCURY_API_NAMESPACE, namesUtil, versionUtil, } from '@sprucelabs/spruce-skill-utils' import { test, assert, generateId } from '@sprucelabs/test-utils' import { generateEventContractFileName } from '../../../features/event/writers/EventWriter' import { FeatureActionResponse } from '../../../features/features.types' import LintService from '../../../services/LintService' import AbstractEventTest from '../../../tests/AbstractEventTest' import testUtil from '../../../tests/utilities/test.utility' import { RegisteredSkill } from '../../../types/cli.types' export default class KeepingEventsInSyncTest extends AbstractEventTest { private static randomVersion = 'v2020_01_01' private static get todaysVersion() { return versionUtil.generateVersion() } @test.skip('find how to simulate connection errors') protected static async syncingSchemasWithDisconnectedStopsWithError() { await this.FeatureFixture().installCachedFeatures('events') await this.syncCoreEventsPretendingToBeMercuryTypes() const results = await this.Action('schema', 'sync').execute({}) const match = testUtil.assertFileByNameInGeneratedFiles( 'sendMessageEmitPayload.schema.ts', results.files ) assert.isTrue(diskUtil.doesFileExist(match)) const client = await this.getMercuryFixture().connectToApi({ shouldAuthAsCurrentSkill: true, }) await client.disconnect() const results2 = await this.Action('schema', 'sync').execute({}) assert.isTruthy(results2.errors) assert.isTrue(diskUtil.doesFileExist(match)) } @test() protected static async syncingSchemasDoesNotSyncEventSchemasIfEventsNotInstalled() { await this.FeatureFixture().installCachedFeatures('schemas') const results = await this.Action('schema', 'sync').execute({}) assert.doesThrow(() => this.assertCorePayloadSchemasAreCreated(results)) } @test() protected static async canGetNumberOfEventsBackFromHealthCheck() { const cli = await this.FeatureFixture().installCachedFeatures('events') LintService.enableLinting() const results = await this.Action('event', 'sync').execute({}) assert.isFalsy(results.errors) await this.Service('build').build() const health = await cli.checkHealth({ shouldRunOnSourceFiles: false }) assert.isTruthy(health.skill) assert.isFalsy(health.skill.errors) assert.isTruthy(health.event) assert.isEqual(health.event.status, 'passed') assert.isTruthy(health.event.contracts) assert.isAbove(health.event.contracts.length, 0) } @test() protected static async syncsEventsFromOtherSkills() { const { skillFixture, skill2 } = await this.seedDummySkillRegisterCurrentSkillAndInstallToOrg() const eventName = `my-new-event::${this.todaysVersion.constValue}` const fqen = `${skill2.slug}.my-new-event::${this.todaysVersion.constValue}` const description = generateId() + '"' + '"' const aiInstructions = generateId() + '"' + '"' await skillFixture.registerEventContract(skill2, { eventSignatures: { [eventName]: { description, aiInstructions, emitPayloadSchema: buildEmitTargetAndPayloadSchema({ eventName: 'my-new-event', targetSchema: eventTargetSchema, payloadSchema: { id: 'myNewEventEmitPayloadId', fields: { onlyField: { type: 'text' } }, }, }), responsePayloadSchema: { id: 'myNewEventResponsePayloadId', fields: {}, }, emitPermissionContract: buildPermissionContract({ id: 'myNewEventEmitPermissionContract', name: 'My new event emit permissionContract', permissions: [ { id: 'can-emit', name: 'Can emit my new event', defaults: { guest: { default: true, }, }, }, ], }), listenPermissionContract: buildPermissionContract({ id: 'myNewEventListenPermissionContract', name: 'My new event listen permissionContract', permissions: [ { id: 'can-listen', name: 'Can emit my new event', defaults: { guest: { default: true, }, }, }, ], }), }, }, }) this.Service('dependency').add({ id: skill2.id, namespace: skill2.slug, }) const results = await this.Action('event', 'sync').execute({}) const match = testUtil.assertFileByNameInGeneratedFiles( `myNewEvent.${this.todaysVersion.dirValue}.contract.ts`, results.files ) assert.doesInclude( match, `${namesUtil.toCamel(skill2.slug)}${pathUtil.sep}myNewEvent.${ this.todaysVersion.dirValue }.contract.ts` ) const contract = (await this.Service('import').importDefault( match )) as any const sig = eventContractUtil.getSignatureByName(contract, fqen) assert.isTruthy(sig.emitPayloadSchema, 'Did not save emitPayloadSchema') assert.isEqual( //@ts-ignore sig.emitPayloadSchema.fields?.payload?.options?.schema?.id, 'myNewEventEmitPayloadId', 'emitPayloadSchema id is wrong' ) assert.isEqual( sig.responsePayloadSchema?.id, 'myNewEventResponsePayloadId', 'responsePayloadSchema id is wrong' ) assert.isTruthy( sig.emitPermissionContract, 'emitPermissionContract missing' ) assert.isEqual( sig.emitPermissionContract.id, `${skill2.slug}.myNewEventEmitPermissionContract`, 'emitPermissionContract id is wrong' ) assert.isEqual( sig.emitPermissionContract.permissions[0].id, 'can-emit', 'emitPermissionContract permission id is wrong' ) assert.isTruthy(sig.listenPermissionContract) assert.isEqual( sig.listenPermissionContract.id, `${skill2.slug}.myNewEventListenPermissionContract`, 'listenPermissionContract id is wrong' ) assert.isEqual( sig.listenPermissionContract.permissions[0].id, 'can-listen', 'listenPermissionContract permission id is wrong' ) assert.isEqual(sig.description, description, 'description missing') assert.isEqual( sig.aiInstructions, aiInstructions, 'aiInstructions missing' ) } @test() protected static async twoSkillsWithSameEventCanBeSynced() { const { skill2, skillFixture, orgFixture, org } = await this.seedDummySkillRegisterCurrentSkillAndInstallToOrg() const skill3 = await skillFixture.seedDemoSkill({ name: 'a third skill', }) await orgFixture.installSkillAtOrganization(skill3.id, org.id) const eventName = `my-new-event::${this.todaysVersion.constValue}` await skillFixture.registerEventContract(skill2, { eventSignatures: { [eventName]: { emitPayloadSchema: buildEmitTargetAndPayloadSchema({ eventName: 'my-new-event', targetSchema: eventTargetSchema, }), }, }, }) await skillFixture.registerEventContract(skill3, { eventSignatures: { [eventName]: { emitPayloadSchema: buildEmitTargetAndPayloadSchema({ eventName: 'my-new-event', targetSchema: eventTargetSchema, }), }, }, }) await this.Service('dependency').set([ { id: skill2.id, namespace: skill2.slug, }, { id: skill3.id, namespace: skill3.slug, }, ]) const results = await this.Action('event', 'sync').execute({}) const contract = testUtil.assertFileByNameInGeneratedFiles( 'events.contract.ts', results.files ) await this.Service('typeChecker').check(contract) } @test() protected static async skillWithSameEventNameButDifferentVersionsCanBeSynced() { const { skill2, skillFixture } = await this.seedDummySkillRegisterCurrentSkillAndInstallToOrg() const eventName = `my-new-event::${this.todaysVersion.constValue}` const eventName2 = `my-new-event::${this.randomVersion}` await skillFixture.registerEventContract(skill2, { eventSignatures: { [eventName]: { emitPayloadSchema: buildEmitTargetAndPayloadSchema({ eventName: 'my-new-event', targetSchema: eventTargetSchema, }), }, [eventName2]: { emitPayloadSchema: buildEmitTargetAndPayloadSchema({ eventName: 'my-new-event', targetSchema: eventTargetSchema, }), }, }, }) await this.Service('dependency').add({ id: skill2.id, namespace: skill2.slug, }) const results = await this.Action('event', 'sync').execute({}) const contract = testUtil.assertFileByNameInGeneratedFiles( 'events.contract.ts', results.files ) await this.Service('typeChecker').check(contract) } @test() protected static async unRegisteredEventsAreRemoved() { const { skillFixture, syncResults, skill2, contractFileName } = await this.seedSkillsAndRegisterAndSyncEvents() await this.assertValidActionResponseFiles(syncResults) await skillFixture.unRegisterEvents(skill2, { shouldUnregisterAll: true, }) const eventContract = testUtil.assertFileByNameInGeneratedFiles( contractFileName, syncResults.files ) assert.isTrue(diskUtil.doesFileExist(eventContract)) await this.Action('event', 'sync').execute({}) assert.isFalse(diskUtil.doesFileExist(eventContract)) const dirname = pathUtil.dirname(eventContract) assert.isFalse(diskUtil.doesDirExist(dirname)) } private static async syncCoreEventsPretendingToBeMercuryTypes() { const results = await this.Action('event', 'sync').execute({ shouldSyncOnlyCoreEvents: true, eventBuilderFile: '../../../builder', skillEventContractTypesFile: '../../builder', }) const builder = ` export function buildEventContract(..._: any[]):any { return _[0] } export function buildPermissionContract(..._: any[]):any { return _[0] } ` diskUtil.writeFile(this.resolvePath('src', 'builder.ts'), builder) await this.Service('pkg').uninstall([ '@sprucelabs/mercury-types', '@sprucelabs/mercury-client', '@sprucelabs/spruce-event-plugin', ]) return results } private static async seedSkillsAndRegisterAndSyncEvents( signature?: EventSignature ) { const { skill2, skillFixture, cli } = await this.seedDummySkillRegisterCurrentSkillAndInstallToOrg() const { results, filename } = await this.registerEventAndSync( skill2, signature ) return { skillFixture, syncResults: results, cli, skill2, contractFileName: filename, } } private static async registerEventAndSync( skill: RegisteredSkill, signature?: EventSignature ) { const skillFixture = this.getSkillFixture() const stamp = new Date().getTime() const eventName = `cleanup-event-test-${stamp}::${this.todaysVersion.constValue}` const filename = generateEventContractFileName({ nameCamel: `cleanupEventTest${stamp}`, version: this.todaysVersion.constValue, }) await skillFixture.registerEventContract(skill, { eventSignatures: { [eventName]: { emitPayloadSchema: buildEmitTargetAndPayloadSchema({ eventName, targetSchema: eventTargetSchema, }), ...signature, }, }, }) await this.Service('dependency').add({ id: skill.id, namespace: skill.slug, }) const results = await this.Action('event', 'sync').execute({}) return { results, filename } } private static assertCorePayloadSchemasAreCreated( results: FeatureActionResponse, shouldHaveWritten = true ) { const filesToCheck = [ { name: `unregisterListenersEmitTargetAndPayload.schema.ts`, path: `schemas${pathUtil.sep}${MERCURY_API_NAMESPACE}`, }, ] this.assertFilesWereGenerated(filesToCheck, results, shouldHaveWritten) } private static assertFilesWereGenerated( filesToCheck: { name: string; path: string }[], results: FeatureActionResponse, shouldHaveWritten = true ) { for (const file of filesToCheck) { const expected = this.resolveHashSprucePath( 'schemas/mercury/v2020_12_25', file.name ) const doesExist = diskUtil.doesFileExist(expected) if (shouldHaveWritten) { assert.isTrue( doesExist, `Expected to find ${file} on the filesystem.` ) } else { assert.isFalse(doesExist, `Should not have written ${file}.`) } } } }