From b3c06915c6cc8679f3e4aa009490af2a683e6621 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Wed, 21 Feb 2024 11:49:03 +0000 Subject: [PATCH] Raise Intent tests --- .../da/src/intents/DefaultIntentSupport.ts | 86 ++++++++++++--- packages/da/src/intents/IntentResolver.ts | 19 ++++ packages/da/test/features/intents.feature | 83 ++++++++++++++ .../test/step-definitions/channels.steps.ts | 4 + .../da/test/step-definitions/generic.steps.ts | 61 ++++++++-- .../da/test/step-definitions/intents.steps.ts | 36 ++++++ packages/da/test/support/TestMessaging.ts | 96 ++++++++++++++-- .../test/support/responses/DefaultResponse.ts | 14 +++ .../da/test/support/responses/FindIntent.ts | 46 ++++++++ .../support/responses/FindIntentByContext.ts | 45 ++++++++ .../da/test/support/responses/RaiseIntent.ts | 91 +++++++++++++++ packages/da/test/temp.json | 104 ------------------ .../private-channels.feature | 38 +++---- 13 files changed, 567 insertions(+), 156 deletions(-) create mode 100644 packages/da/src/intents/IntentResolver.ts create mode 100644 packages/da/test/features/intents.feature create mode 100644 packages/da/test/step-definitions/intents.steps.ts create mode 100644 packages/da/test/support/responses/DefaultResponse.ts create mode 100644 packages/da/test/support/responses/FindIntent.ts create mode 100644 packages/da/test/support/responses/FindIntentByContext.ts create mode 100644 packages/da/test/support/responses/RaiseIntent.ts delete mode 100644 packages/da/test/temp.json rename packages/da/test/{features => unused-features}/private-channels.feature (79%) diff --git a/packages/da/src/intents/DefaultIntentSupport.ts b/packages/da/src/intents/DefaultIntentSupport.ts index 3f8acb0f4..26b85126d 100644 --- a/packages/da/src/intents/DefaultIntentSupport.ts +++ b/packages/da/src/intents/DefaultIntentSupport.ts @@ -1,38 +1,46 @@ -import { Context, AppIntent, AppIdentifier, IntentResolution, IntentHandler, Listener } from "@finos/fdc3"; +import { Context, AppIntent, AppIdentifier, IntentResolution, IntentHandler, Listener, ResolveError } from "@finos/fdc3"; import { IntentSupport } from "./IntentSupport"; import { Messaging } from "../Messaging"; import { AppDestinationIdentifier, FindIntentAgentRequest, FindIntentAgentRequestMeta, FindIntentAgentResponse, FindIntentsByContextAgentRequest, FindIntentsByContextAgentRequestMeta, FindIntentsByContextAgentResponse, RaiseIntentAgentRequest, RaiseIntentAgentRequestMeta, RaiseIntentAgentResponse, RaiseIntentResultAgentResponse } from "@finos/fdc3/dist/bridging/BridgingTypes"; import { DefaultIntentResolution } from "./DefaultIntentResolution"; import { DefaultIntentListener } from "../listeners/DefaultIntentListener"; +import { IntentResolver } from "./IntentResolver"; export class DefaultIntentSupport implements IntentSupport { - + readonly messaging: Messaging + readonly intentResolver: IntentResolver - constructor(messaging: Messaging) { + constructor(messaging: Messaging, intentResolver: IntentResolver) { this.messaging = messaging + this.intentResolver = intentResolver } - async findIntent(intent: string, context: Context, _resultType: string | undefined): Promise { - const messageOut : FindIntentAgentRequest = { + async findIntent(intent: string, context: Context, resultType: string | undefined): Promise { + const messageOut: FindIntentAgentRequest = { type: "findIntentRequest", payload: { intent, - context + context, + resultType }, meta: this.messaging.createMeta() as FindIntentAgentRequestMeta } const result = await this.messaging.exchange(messageOut, "findIntentResponse") as FindIntentAgentResponse + if (result.payload.appIntent.apps.length == 0) { + throw new Error(ResolveError.NoAppsFound) + } + return { intent: result.payload.appIntent.intent, apps: result.payload.appIntent.apps } } - + async findIntentsByContext(context: Context): Promise { - const messageOut : FindIntentsByContextAgentRequest = { + const messageOut: FindIntentsByContextAgentRequest = { type: "findIntentsByContextRequest", payload: { context @@ -42,11 +50,15 @@ export class DefaultIntentSupport implements IntentSupport { const result = await this.messaging.exchange(messageOut, "findIntentsByContextResponse") as FindIntentsByContextAgentResponse + if (result.payload.appIntents.length == 0) { + throw new Error(ResolveError.NoAppsFound) + } + return result.payload.appIntents } - async raiseIntentInner(intent: string | undefined, context: Context, app: AppIdentifier | undefined) : Promise { - const messageOut : RaiseIntentAgentRequest = { + private async raiseSpecificIntent(intent: string, context: Context, app: AppIdentifier): Promise { + const messageOut: RaiseIntentAgentRequest = { type: "raiseIntentRequest", payload: { intent: intent as any, // raised #1157 @@ -57,7 +69,7 @@ export class DefaultIntentSupport implements IntentSupport { } const resolution = await this.messaging.exchange(messageOut, "raiseIntentResponse") as RaiseIntentAgentResponse - const details = resolution.payload.intentResolution + const details = resolution.payload.intentResolution const resultPromise = this.messaging.waitFor( m => m.meta.requestUuid == messageOut.meta.requestUuid @@ -65,23 +77,65 @@ export class DefaultIntentSupport implements IntentSupport { return new DefaultIntentResolution( this.messaging, - resultPromise, + resultPromise, details.source, details.intent, details.version ) } + private matchAppId(a: AppIdentifier, b: AppIdentifier) { + return (a.appId == b.appId) && ((a.instanceId == null) || a.instanceId == b.instanceId) + } + + private filterApps(appIntent: AppIntent, app: AppIdentifier): AppIntent { + return { + apps: appIntent.apps.filter(a => this.matchAppId(a, app)), + intent: appIntent.intent + } + } + async raiseIntent(intent: string, context: Context, app?: AppIdentifier | undefined): Promise { - return this.raiseIntentInner(intent, context, app) + var matched = await this.findIntent(intent, context, undefined) + + if (app) { + // ensure app matches + matched = this.filterApps(matched, app) + } + + if (matched.apps.length == 0) { + throw new Error(ResolveError.NoAppsFound) + } else if (matched.apps.length == 1) { + return this.raiseSpecificIntent(intent, context, matched.apps[0]) + } else { + // need to do the intent resolver + const chosentIntent = await this.intentResolver.resolveIntent([matched]) + return this.raiseSpecificIntent(intent, context, chosentIntent.chosenApp) + } } - raiseIntentForContext(context: Context, app?: AppIdentifier | undefined): Promise { - return this.raiseIntentInner(undefined, context, app) + async raiseIntentForContext(context: Context, app?: AppIdentifier | undefined): Promise { + var matched = await this.findIntentsByContext(context) + + if (app) { + matched = matched + .map(m => this.filterApps(m, app)) + .filter(m => m.apps.length == 0) + } + + if (matched.length == 0) { + throw new Error(ResolveError.NoAppsFound) + } else if ((matched.length == 1) && (matched[0].apps.length == 1)) { + return this.raiseSpecificIntent(matched[0].intent.name, context, matched[0].apps[0]) + } else { + // need to do the intent resolver + const chosentIntent = await this.intentResolver.resolveIntent(matched) + return this.raiseSpecificIntent(chosentIntent.intent.name, context, chosentIntent.chosenApp) + } } async addIntentListener(intent: string, handler: IntentHandler): Promise { return new DefaultIntentListener(this.messaging, intent, handler); } - + } \ No newline at end of file diff --git a/packages/da/src/intents/IntentResolver.ts b/packages/da/src/intents/IntentResolver.ts new file mode 100644 index 000000000..0c4faeb9d --- /dev/null +++ b/packages/da/src/intents/IntentResolver.ts @@ -0,0 +1,19 @@ +import { AppIdentifier, AppIntent, IntentMetadata } from "@finos/fdc3"; + +/** + * Contains the details of a single intent and application resolved + * by the IntentResolver implementation + */ +export interface SingleAppIntent { + + intent: IntentMetadata + chosenApp: AppIdentifier + +} + +export interface IntentResolver { + + resolveIntent(appIntents: AppIntent[]) : SingleAppIntent + +} + diff --git a/packages/da/test/features/intents.feature b/packages/da/test/features/intents.feature new file mode 100644 index 000000000..5c5ee9bfa --- /dev/null +++ b/packages/da/test/features/intents.feature @@ -0,0 +1,83 @@ +Feature: Basic Intents Support + + Background: Desktop Agent API + + Given A Desktop Agent in "api" + + And app "chipShop/c1" resolves intent "OrderFood" with result type "void" + And app "chipShop/c2" resolves intent "OrderFood" with result type "channel" + And app "library/l1" resolves intent "BorrowBooks" with result type "channel" + And app "bank/b1" resolves intent "Buy" with context "fdc3.instrument" and result type "fdc3.order" + And app "travelAgent/t1" resolves intent "Buy" with context "fdc3.currency" and result type "fdc3.order" + + And "instrumentContext" is a "fdc3.instrument" context + And "crazyContext" is a "fdc3.unsupported" context + + Scenario: Find Intent can return the same intent with multiple apps + + When I call "api" with "findIntent" with parameter "Buy" + Then "{result.intent}" is an object with the following contents + | name | + | Buy | + And "{result.apps}" is an array of objects with the following contents + | appId | instanceId | + | bank | b1 | + | travelAgent | t1 | + + Scenario: Find Intent can return an error when an intent doesn't match + + When I call "api" with "findIntent" with parameter "Bob" + Then "result" is an error with message "NoAppsFound" + + Scenario: Find Intent can filter by a context type + + When I call "api" with "findIntent" with parameters "Buy" and "{instrumentContext}" + Then "{result.intent}" is an object with the following contents + | name | + | Buy | + And "{result.apps}" is an array of objects with the following contents + | appId | + | bank | + + Scenario: Find Intent can filter by generic result Type + + When I call "api" with "findIntent" with parameters "OrderFood" and "{empty}" and "channel" + Then "{result.intent}" is an object with the following contents + | name | + | OrderFood | + And "{result.apps}" is an array of objects with the following contents + | appId | instanceId | + | chipShop | c2 | + + Scenario: Find Intents By Context + + When I call "api" with "findIntentsByContext" with parameter "{instrumentContext}" + Then "{result}" is an array of objects with the following contents + | intent.name | apps[0].appId | apps.length | + | Buy | bank | 1 | + + Scenario: Find Intents By Context can return an error when an intent doesn't match + + When I call "api" with "findIntentsByContext" with parameter "{crazyContext}" + Then "result" is an error with message "NoAppsFound" + + Scenario: Raising A Specific Intent to the server, returning a context object + + When I call "api" with "raiseIntent" with parameters "Buy" and "{instrumentContext}" and "{b1}" + Then "{result}" is an object with the following contents + | source.appId | source.instanceId | intent | + | bank | b1 | Buy | + And I call "result" with "getResult" + Then "{result}" is an object with the following contents + | type | name | + | fdc3.order | Big Order 9 | + + Scenario: Raising An Invalid Intent to the server + + When I call "api" with "raiseIntent" with parameters "Buy" and "{instrumentContext}" and "{c1}" + Then "result" is an error with message "NoAppsFound" + + # Scenario: Invoking the intent resolver when it's not clear which intent is required + + # When I call "api" with "raiseIntent" with parameters "OrderFood" and "{instrumentContext}" + diff --git a/packages/da/test/step-definitions/channels.steps.ts b/packages/da/test/step-definitions/channels.steps.ts index c7f9b4a8f..4cc2d7c4a 100644 --- a/packages/da/test/step-definitions/channels.steps.ts +++ b/packages/da/test/step-definitions/channels.steps.ts @@ -19,6 +19,10 @@ const contextMap : Record = { "COUNTRY_ISOALPHA2": "SE", "COUNTRY_ISOALPHA3": "SWE", } + }, + "fdc3.unsupported" : { + "type": "fdc3.unsupported", + "bogus": true } } diff --git a/packages/da/test/step-definitions/generic.steps.ts b/packages/da/test/step-definitions/generic.steps.ts index b05ac3309..1da73c2da 100644 --- a/packages/da/test/step-definitions/generic.steps.ts +++ b/packages/da/test/step-definitions/generic.steps.ts @@ -5,6 +5,31 @@ import { expect } from 'expect'; import { doesRowMatch, handleResolve, matchData } from '../support/matching'; import { CustomWorld } from '../world/index'; import { BasicDesktopAgent, DefaultAppSupport, DefaultChannelSupport, DefaultIntentSupport } from '../../src'; +import { IntentResolver, SingleAppIntent } from '../intents/IntentResolver'; +import { AppIntent } from '@finos/fdc3'; + +/** + * This super-simple intent resolver just resolves to the first + * intent / app in the list. + */ +class SimpleIntentResolver implements IntentResolver { + + constructor(cw: CustomWorld) { + this.cw = cw; + } + + cw: CustomWorld + + resolveIntent(appIntents: AppIntent[]): SingleAppIntent { + const out = { + intent: appIntents[0].intent, + chosenApp :appIntents[0].apps[0] + } + + this.cw.props['intent-resolution'] = out + return out + } +} Given('A Desktop Agent in {string}', function (this: CustomWorld, field: string) { @@ -14,7 +39,7 @@ Given('A Desktop Agent in {string}', function (this: CustomWorld, field: string) this.props[field] = new BasicDesktopAgent ( new DefaultChannelSupport(this.messaging, createDefaultChannels(this.messaging), null), - new DefaultIntentSupport(this.messaging), + new DefaultIntentSupport(this.messaging, new SimpleIntentResolver(this)), new DefaultAppSupport(this.messaging, { appId: "Test App Id", desktopAgent: "Test DA", @@ -28,9 +53,13 @@ Given('A Desktop Agent in {string}', function (this: CustomWorld, field: string) }) When('I call {string} with {string}', async function (this: CustomWorld, field: string, fnName: string) { - const fn = this.props[field][fnName]; - const result = await fn.call(this.props[field]) - this.props['result'] = result; + try { + const fn = this.props[field][fnName]; + const result = await fn.call(this.props[field]) + this.props['result'] = result; + } catch (error) { + this.props['result'] = error + } }) When('I call {string} with {string} with parameter {string}', async function (this: CustomWorld, field: string, fnName: string, param: string) { @@ -44,9 +73,23 @@ When('I call {string} with {string} with parameter {string}', async function (th }) When('I call {string} with {string} with parameters {string} and {string}', async function (this: CustomWorld, field: string, fnName: string, param1: string, param2: string) { - const fn = this.props[field][fnName]; - const result = await fn.call(this.props[field], handleResolve(param1, this), handleResolve(param2, this)) - this.props['result'] = result; + try { + const fn = this.props[field][fnName]; + const result = await fn.call(this.props[field], handleResolve(param1, this), handleResolve(param2, this)) + this.props['result'] = result; + } catch (error) { + this.props['result'] = error + } +}); + +When('I call {string} with {string} with parameters {string} and {string} and {string}', async function (this: CustomWorld, field: string, fnName: string, param1: string, param2: string, param3: string) { + try { + const fn = this.props[field][fnName]; + const result = await fn.call(this.props[field], handleResolve(param1, this), handleResolve(param2, this), handleResolve(param3, this)) + this.props['result'] = result; + } catch (error) { + this.props['result'] = error + } }); When('I refer to {string} as {string}', async function (this: CustomWorld, from: string, to: string) { @@ -54,7 +97,7 @@ When('I refer to {string} as {string}', async function (this: CustomWorld, from: }) Then('{string} is an array of objects with the following contents', function (this: CustomWorld, field: string, dt: DataTable) { - matchData(this, this.props[field], dt) + matchData(this, handleResolve(field, this), dt) }); Then('{string} is an array of strings with the following values', function (this: CustomWorld, field: string, dt: DataTable) { @@ -64,7 +107,7 @@ Then('{string} is an array of strings with the following values', function (this Then('{string} is an object with the following contents', function (this: CustomWorld, field: string, params: DataTable) { const table = params.hashes() - expect(doesRowMatch(this, table[0], this.props[field])).toBeTruthy(); + expect(doesRowMatch(this, table[0], handleResolve(field, this))).toBeTruthy(); }); Then('{string} is null', function (this: CustomWorld, field: string) { diff --git a/packages/da/test/step-definitions/intents.steps.ts b/packages/da/test/step-definitions/intents.steps.ts new file mode 100644 index 000000000..b7a98f657 --- /dev/null +++ b/packages/da/test/step-definitions/intents.steps.ts @@ -0,0 +1,36 @@ +import { Given } from '@cucumber/cucumber' +import { CustomWorld } from '../world/index'; + +Given("app {string} resolves intent {string} with result type {string}", function (this: CustomWorld, appStr: string, intent: string, resultType: string) { + const [ appId, instanceId ] = appStr.split("/") + const app = { appId, instanceId } + this.messaging?.addAppIntentDetail({ + app, + intent, + resultType + }) + this.props[instanceId] = app +}) + +Given("app {string} resolves intent {string} with context {string}", function (this: CustomWorld, appStr: string, intent: string, context: string) { + const [ appId, instanceId ] = appStr.split("/") + const app = { appId, instanceId } + this.messaging?.addAppIntentDetail({ + app, + intent, + context + }) + this.props[instanceId] = app +}) + +Given("app {string} resolves intent {string} with context {string} and result type {string}", function (this: CustomWorld, appStr: string, intent: string, context: string, resultType: string) { + const [ appId, instanceId ] = appStr.split("/") + const app = { appId, instanceId } + this.messaging?.addAppIntentDetail({ + app, + intent, + context, + resultType + }) + this.props[instanceId] = app +}) \ No newline at end of file diff --git a/packages/da/test/support/TestMessaging.ts b/packages/da/test/support/TestMessaging.ts index 1170be71c..30e73d07f 100644 --- a/packages/da/test/support/TestMessaging.ts +++ b/packages/da/test/support/TestMessaging.ts @@ -1,14 +1,80 @@ -import { ICreateLog } from "@cucumber/cucumber/lib/runtime/attachment_manager"; import { AppIdentifier } from "@finos/fdc3"; -import { AgentRequestMessage } from "@finos/fdc3/dist/bridging/BridgingTypes"; +import { AgentRequestMessage, AgentResponseMessage } from "@finos/fdc3/dist/bridging/BridgingTypes"; import { v4 as uuidv4 } from 'uuid' import { AbstractMessaging } from "../../src/messaging/AbstractMessaging"; -import { RegisterableListener } from "../../src/listeners/RegisterableListener"; +import { RegisterableListener } from "../listeners/RegisterableListener"; +import { FindIntent } from "./responses/FindIntent"; +import { FIndIntentByContext } from "./responses/FindIntentByContext"; +import { ICreateLog } from "@cucumber/cucumber/lib/runtime/attachment_manager"; +import { RaiseIntent } from "./responses/RaiseIntent"; + +export interface IntentDetail { + app?: AppIdentifier, + intent?: string, + context?: string, + resultType?: string +} + +export interface AutomaticResponse { + + filter: (t: string) => boolean, + action: (input: AgentRequestMessage, m: TestMessaging) => Promise + +} + +function matchStringOrUndefined(expected: string | undefined, actual: string | undefined) { + if (expected) { + return expected == actual + } else { + return true + } +} + +function removeGenericType(t: string) { + const startOfGeneric = t.indexOf("<") + if (startOfGeneric > -1) { + return t.substring(0, startOfGeneric-1) + } else { + return t + } +} + +function matchResultTypes(expected: string | undefined, actual: string | undefined) { + if (expected) { + if (expected.indexOf("<") > -1) { + // looking for a complete match involving generics + return expected == actual + } else if (actual == undefined) { + // no actual, only expected + return false; + } else { + // expected doesn't have generics, match without + const actualType = removeGenericType(actual) + return expected == actualType + } + } else { + return true; + } +} + +export function intentDetailMatches(instance: IntentDetail, template: IntentDetail) : boolean { + return matchStringOrUndefined(template.app?.appId, instance.app?.appId) && + matchStringOrUndefined(template.app?.instanceId, instance.app?.instanceId) && + matchStringOrUndefined(template.intent, instance.intent) && + matchStringOrUndefined(template.context, instance.context) && + matchResultTypes(template.resultType, instance.resultType) +} export class TestMessaging extends AbstractMessaging { readonly allPosts : AgentRequestMessage[] = [] readonly listeners : Map = new Map() + readonly intentDetails : IntentDetail[] = [] + readonly automaticResponses : AutomaticResponse[] = [ + new FindIntent(), + new FIndIntentByContext(), + new RaiseIntent() + ] getSource(): AppIdentifier { return { @@ -21,11 +87,24 @@ export class TestMessaging extends AbstractMessaging { return uuidv4() } + post(message: AgentRequestMessage): Promise { - this.allPosts.push(message); + this.allPosts.push(message) + + for (let i = 0; i < this.automaticResponses.length; i++) { + const ar = this.automaticResponses[i] + if (ar.filter(message.type)) { + return ar.action(message, this) + } + } + return Promise.resolve(); } + addAppIntentDetail(id: IntentDetail) { + this.intentDetails.push(id) + } + register(l: RegisterableListener) { this.listeners.set(l.id, l) } @@ -38,17 +117,18 @@ export class TestMessaging extends AbstractMessaging { return { "requestUuid": this.createUUID(), "timestamp": new Date(), - "source": this.getSource() + "source": this.getSource(), + "responseUuid": this.createUUID() } } - receive(m: AgentRequestMessage, log: ICreateLog) { + receive(m: AgentRequestMessage | AgentResponseMessage, log?: ICreateLog) { this.listeners.forEach((v, k) => { if (v.filter(m)) { - log("Processing in "+k) + log ? log("Processing in "+k) : "" v.action(m) } else { - log("Ignoring in "+k) + log ? log("Ignoring in "+k) : "" } }) } diff --git a/packages/da/test/support/responses/DefaultResponse.ts b/packages/da/test/support/responses/DefaultResponse.ts new file mode 100644 index 000000000..101caf4be --- /dev/null +++ b/packages/da/test/support/responses/DefaultResponse.ts @@ -0,0 +1,14 @@ +import { AgentRequestMessage } from "@finos/fdc3/dist/bridging/BridgingTypes"; +import { AutomaticResponse, TestMessaging } from "../TestMessaging"; + +export class DefaultResponse implements AutomaticResponse { + + filter(t: string) { + return true + } + + action(_input: AgentRequestMessage, _m: TestMessaging) { + return Promise.resolve() + } + +} \ No newline at end of file diff --git a/packages/da/test/support/responses/FindIntent.ts b/packages/da/test/support/responses/FindIntent.ts new file mode 100644 index 000000000..d5c1899a3 --- /dev/null +++ b/packages/da/test/support/responses/FindIntent.ts @@ -0,0 +1,46 @@ +import { AgentRequestMessage, FindIntentAgentRequest, FindIntentAgentResponse, FindIntentAgentResponseMeta } from "@finos/fdc3/dist/bridging/BridgingTypes"; +import { AutomaticResponse, IntentDetail, TestMessaging, intentDetailMatches } from "../TestMessaging"; + +export class FindIntent implements AutomaticResponse { + + filter(t: string) { + return t == 'findIntentRequest' + } + + action(input: AgentRequestMessage, m: TestMessaging) { + const intentRequest = input as FindIntentAgentRequest + const payload = intentRequest.payload + const intent = payload.intent + const context = payload?.context?.type + const resultType = payload?.resultType; + const template : IntentDetail = { + intent, + context, + resultType + } + + const relevant = m.intentDetails.filter(id => intentDetailMatches(id, template)) + const request = this.createFindIntentResponseMessage(intentRequest, relevant) + setTimeout(() => { m.receive(request) },100) + return Promise.resolve() + } + + private createFindIntentResponseMessage(m: FindIntentAgentRequest, relevant: IntentDetail[]) : FindIntentAgentResponse { + return { + meta: m.meta as FindIntentAgentResponseMeta, + type: "findIntentResponse", + payload: { + appIntent: { + apps: relevant.map(r => { return { + appId: r?.app?.appId!!, + instanceId: r?.app?.instanceId + }}), + intent: { + displayName: m.payload.intent, + name: m.payload.intent + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/da/test/support/responses/FindIntentByContext.ts b/packages/da/test/support/responses/FindIntentByContext.ts new file mode 100644 index 000000000..116aadbd3 --- /dev/null +++ b/packages/da/test/support/responses/FindIntentByContext.ts @@ -0,0 +1,45 @@ +import { AgentRequestMessage, FindIntentsByContextAgentRequest, FindIntentsByContextAgentResponse, FindIntentsByContextAgentResponseMeta } from "@finos/fdc3/dist/bridging/BridgingTypes"; +import { AutomaticResponse, IntentDetail, TestMessaging, intentDetailMatches } from "../TestMessaging"; + + +export class FIndIntentByContext implements AutomaticResponse { + + filter(t: string) { + return t == 'findIntentsByContextRequest' + } + + action(input: AgentRequestMessage, m: TestMessaging) { + const intentRequest = input as FindIntentsByContextAgentRequest + const payload = intentRequest.payload + const context = payload?.context?.type + const template: IntentDetail = { + context + } + + const relevant = m.intentDetails.filter(id => intentDetailMatches(id, template)) + const request = this.createFindIntentsByContextResponseMessage(intentRequest, relevant) + setTimeout(() => { m.receive(request) }, 100) + return Promise.resolve() + } + + + private createFindIntentsByContextResponseMessage(m: FindIntentsByContextAgentRequest, relevant: IntentDetail[]): FindIntentsByContextAgentResponse { + const relevantIntents = [...new Set(relevant.map(r => r.intent!!))] + + return { + meta: m.meta as FindIntentsByContextAgentResponseMeta, + type: "findIntentsByContextResponse", + payload: { + appIntents: + relevantIntents.map(i => { + return { + intent: { name: i, displayName: i }, + apps: relevant + .filter(r => r.intent == i) + .map(r => r.app!!)!! + } + }) + } + } + } +} \ No newline at end of file diff --git a/packages/da/test/support/responses/RaiseIntent.ts b/packages/da/test/support/responses/RaiseIntent.ts new file mode 100644 index 000000000..91634a444 --- /dev/null +++ b/packages/da/test/support/responses/RaiseIntent.ts @@ -0,0 +1,91 @@ +import { AgentRequestMessage, IntentResult, RaiseIntentAgentRequest, RaiseIntentAgentResponse, RaiseIntentResultAgentResponse } from "@finos/fdc3/dist/bridging/BridgingTypes"; +import { AutomaticResponse, IntentDetail, TestMessaging, intentDetailMatches } from "../TestMessaging"; + + +export class RaiseIntent implements AutomaticResponse { + + filter(t: string) { + return t == 'raiseIntentRequest' + } + + createRaiseIntentAgentResponseMessage(intentRequest: RaiseIntentAgentRequest, using: IntentDetail, m: TestMessaging) : RaiseIntentAgentResponse { + const out : RaiseIntentAgentResponse = { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + payload: { + intentResolution: { + intent: using.intent!!, + source: using.app!! + } + }, + type: "raiseIntentResponse" + } + + return out + } + + createRaiseIntentResultResponseMesssage(intentRequest: RaiseIntentAgentRequest, using: IntentDetail, m: TestMessaging) : RaiseIntentResultAgentResponse { + var intentResult : IntentResult = {} + + // here, we're just providing a few canned responses required for the tests + switch (using.resultType) { + case "channel": + intentResult.channel = { + type: 'app', + id: 'result-channel', + displayMetadata: { + color: "purple", + name: "Result Channel" + } + } + break; + case "fdc3.order": + intentResult.context = { + type: 'fdc3.order', + id: { + myOMS: "OMS-9", + }, + name: "Big Order 9" + } + break; + } + + const out: RaiseIntentResultAgentResponse = { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + payload: { + intentResult + }, + type: "raiseIntentResultResponse" + } + + return out + } + + action(input: AgentRequestMessage, m: TestMessaging) { + const intentRequest = input as RaiseIntentAgentRequest + const payload = intentRequest.payload + const intent = payload.intent + const context = payload?.context?.type + const template: IntentDetail = { + intent, + context + } + + const relevant = m.intentDetails.filter(id => intentDetailMatches(id, template)) + const using = relevant[0] + + // this sends out the intent resolution + const out1 = this.createRaiseIntentAgentResponseMessage(intentRequest, using, m) + setTimeout(() => { m.receive(out1) }, 100) + + // next, send the result response + const out2 = this.createRaiseIntentResultResponseMesssage(intentRequest, using, m) + setTimeout(() => { m.receive(out2) }, 300) + return Promise.resolve() + } +} diff --git a/packages/da/test/temp.json b/packages/da/test/temp.json deleted file mode 100644 index 2d55614c2..000000000 --- a/packages/da/test/temp.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "meta": { - "requestUuid": "bae5e83d-bf3b-45f3-a9d8-2b2bb9a03e5e", - "source": { - "appId": "SomeDummyApp", - "instanceId": "some.dummy.instance" - }, - "timestamp": "2024-02-17T12:13:35.802Z" - }, - "payload": { - "channelId": "18d73690-d84e-414d-bd93-e49ed26c774f", - "listenerType": "onAddContextListener" - }, - "type": "PrivateChannel.eventListenerAdded" -}, -{ - "meta": { - "requestUuid": "d769a916-cd0f-49c6-9ec2-08b4e42c5364", - "source": { - "appId": "SomeDummyApp", - "instanceId": "some.dummy.instance" - }, - "timestamp": "2024-02-17T12:13:35.802Z" - }, - "payload": { - "channelId": "18d73690-d84e-414d-bd93-e49ed26c774f", - "listenerType": "onAddContextListener" - }, - "type": "PrivateChannel.eventListenerAdded" -}, -{ - "meta": { - "requestUuid": "f377dfd4-28ef-4da7-a76f-ac17fd94846f", - "source": { - "appId": "SomeDummyApp", - "instanceId": "some.dummy.instance" - }, - "timestamp": "2024-02-17T12:13:35.802Z" - }, - "payload": { - "channelId": "18d73690-d84e-414d-bd93-e49ed26c774f", - "listenerType": "onUnsubscribe" - }, - "type": "PrivateChannel.eventListenerAdded" -}, -{ - "meta": { - "requestUuid": "66b8d5cb-dba5-4f57-8f7b-fb5e4ffbdf28", - "source": { - "appId": "SomeDummyApp", - "instanceId": "some.dummy.instance" - }, - "timestamp": "2024-02-17T12:13:35.802Z" - }, - "payload": { - "channelId": "18d73690-d84e-414d-bd93-e49ed26c774f", - "listenerType": "onUnsubscribe" - }, - "type": "PrivateChannel.eventListenerAdded" -}, -{ - "meta": { - "requestUuid": "72b5f9af-69d7-40df-bc95-7a95150b3395", - "source": { - "appId": "SomeDummyApp", - "instanceId": "some.dummy.instance" - }, - "timestamp": "2024-02-17T12:13:35.803Z" - }, - "payload": { - "channelId": "18d73690-d84e-414d-bd93-e49ed26c774f", - "listenerType": "onAddContextListener" - }, - "type": "PrivateChannel.eventListenerRemoved" -}, -{ - "meta": { - "requestUuid": "5aa36192-4ab1-49d6-a24a-22a2c76b7fa7", - "source": { - "appId": "SomeDummyApp", - "instanceId": "some.dummy.instance" - }, - "timestamp": "2024-02-17T12:13:35.803Z" - }, - "payload": { - "channelId": "18d73690-d84e-414d-bd93-e49ed26c774f", - "listenerType": "onUnsubscribe" - }, - "type": "PrivateChannel.eventListenerRemoved" -}, -{ - "meta": { - "requestUuid": "5842dfb5-a121-46b7-a443-75727efb91b5", - "source": { - "appId": "SomeDummyApp", - "instanceId": "some.dummy.instance" - }, - "timestamp": "2024-02-17T12:13:35.803Z" - }, - "payload": { - "channelId": "18d73690-d84e-414d-bd93-e49ed26c774f" - }, - "type": "PrivateChannel.onDisconnect" -} \ No newline at end of file diff --git a/packages/da/test/features/private-channels.feature b/packages/da/test/unused-features/private-channels.feature similarity index 79% rename from packages/da/test/features/private-channels.feature rename to packages/da/test/unused-features/private-channels.feature index 2513ab423..0e4a2f32e 100644 --- a/packages/da/test/features/private-channels.feature +++ b/packages/da/test/unused-features/private-channels.feature @@ -6,25 +6,25 @@ Background: Desktop Agent API And I call "api" with "createPrivateChannel" And I refer to "result" as "privateChannel" - # Scenario: Adding and then unsubscribing a context listener will send a notification of each event to the agent - - # Given "contextHandler" pipes context to "context" - # When I call "privateChannel" with "addContextListener" with parameters "fdc3.instrument" and "{contextHandler}" - # And I call "result" with "unsubscribe" - # Then messaging will have posts - # | type | payload.channelId | payload.contextType | - # | PrivateChannel.onAddContextListener | {privateChannel.id} | fdc3.instrument | - # | PrivateChannel.onUnsubscribe | {privateChannel.id} | fdc3.instrument | - - # Scenario: Adding a Context Listener on a given Private Channel to receive a notification - - # Given "instrumentMessageOne" is a "PrivateChannel.broadcast" message on channel "{privateChannel.id}" with context "fdc3.instrument" - # And "resultHandler" pipes context to "contexts" - # When I call "privateChannel" with "addContextListener" with parameters "fdc3.instrument" and "{resultHandler}" - # And messaging receives "{instrumentMessageOne}" - # Then "contexts" is an array of objects with the following contents - # | id.ticker | type | name | - # | AAPL | fdc3.instrument | Apple | + Scenario: Adding and then unsubscribing a context listener will send a notification of each event to the agent + + Given "contextHandler" pipes context to "context" + When I call "privateChannel" with "addContextListener" with parameters "fdc3.instrument" and "{contextHandler}" + And I call "result" with "unsubscribe" + Then messaging will have posts + | type | payload.channelId | payload.contextType | + | PrivateChannel.onAddContextListener | {privateChannel.id} | fdc3.instrument | + | PrivateChannel.onUnsubscribe | {privateChannel.id} | fdc3.instrument | + + Scenario: Adding a Context Listener on a given Private Channel to receive a notification + + Given "instrumentMessageOne" is a "PrivateChannel.broadcast" message on channel "{privateChannel.id}" with context "fdc3.instrument" + And "resultHandler" pipes context to "contexts" + When I call "privateChannel" with "addContextListener" with parameters "fdc3.instrument" and "{resultHandler}" + And messaging receives "{instrumentMessageOne}" + Then "contexts" is an array of objects with the following contents + | id.ticker | type | name | + | AAPL | fdc3.instrument | Apple | Scenario: Adding and then unsubscribing an "onAddContextListener" listener will send a notification of each event to the agent