-
Notifications
You must be signed in to change notification settings - Fork 15
feat: align preview ContextVariable with typed API schema @W-24014400@ #356
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
base: main
Are you sure you want to change the base?
Changes from all commits
f14e733
63097e9
d62adc9
f558d65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,11 +49,29 @@ export type AgentPreviewInterface = { | |
| setApexDebugging: (apexDebugging: boolean) => void; | ||
| }; | ||
|
|
||
| export type ContextVariable = { | ||
| name: string; | ||
| type: 'Object' | 'Boolean' | 'DateTime' | 'Money' | 'Number' | 'Text' | 'Ref' | 'List'; | ||
| value: string; | ||
| }; | ||
| // The `type` discriminator for a ContextVariable, matching the preview API's | ||
| // `Variable.type` enum (agent-api v1.1 OpenAPI schema `Variable`). | ||
| export type ContextVariableType = 'Text' | 'Date' | 'DateTime' | 'Money' | 'Ref' | 'Boolean' | 'Number' | 'Object' | 'List' | 'Json'; | ||
|
|
||
| /** | ||
| * A context/custom variable sent when starting a preview session or a message. | ||
| * | ||
| * This mirrors the preview API's polymorphic `Variable` schema: the JSON type of | ||
| * `value` is determined by `type`. Boolean values are booleans, Number values are | ||
| * numbers, Object/List values are arrays, and Json is any JSON object. Sending a | ||
| * boolean as the string "True" (the old behavior) leaves a boolean-gated route | ||
| * closed, because the runtime compares "True" (Text) against True (Boolean). | ||
| * | ||
| * `value` is optional and nullable to match the API, where only `name` and `type` | ||
| * are required. | ||
| */ | ||
| export type ContextVariable = | ||
| | { name: string; type: 'Boolean'; value?: boolean | null } | ||
| | { name: string; type: 'Number'; value?: number | null } | ||
| | { name: string; type: 'Text' | 'Date' | 'DateTime' | 'Money' | 'Ref'; value?: string | null } | ||
| | { name: string; type: 'Object'; value?: ContextVariable[] | null } | ||
| | { name: string; type: 'List'; value?: Array<Record<string, unknown>> | null } | ||
| | { name: string; type: 'Json'; value?: Record<string, unknown> | null }; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could have gone with a looser single value union but having the context variables clearly defined here is the better trade off. A bit more code but much clearer about what is supported. |
||
|
|
||
| export type AgentPreviewStartOptions = { | ||
| contextVariables?: ContextVariable[]; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -306,7 +306,46 @@ describe('agent NUTs', () => { | |
| // Set the default agent user in the agent script so we can test live preview | ||
| const aabDir = join(session.project.dir, 'force-app', 'main', 'default', 'aiAuthoringBundles', bundleApiName); | ||
| const agentScriptFile = readFileSync(join(aabDir, 'Test_AAB_Preview.agent'), 'utf8'); | ||
| const updatedAgentScriptFile = agentScriptFile.replace('NEW AGENT USER', defaultAgentUsername); | ||
| const updatedAgentScriptFile = agentScriptFile | ||
| .replace('NEW AGENT USER', defaultAgentUsername) | ||
| // Declare one External, settable var per wire type so the typed-context-variable | ||
| // round-trip test below has a target for each. External visibility is what makes a | ||
| // mutable var settable via preview; without it the server rejects the mutation. | ||
| .replace( | ||
| ' description: "This variable may also be referred to as VerifiedCustomerId"', | ||
| [ | ||
| ' description: "This variable may also be referred to as VerifiedCustomerId"', | ||
| ' NutProbeText: mutable string = ""', | ||
| ' description: "Test-only External var: a Text value set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| ' NutProbeBool: mutable boolean = False', | ||
| ' description: "Test-only External var: a Boolean value set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| ' NutProbeNum: mutable number = 0', | ||
| ' description: "Test-only External var: a Number value set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| ' NutProbeObj: mutable object = {}', | ||
| ' description: "Test-only External var: a JSON object set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| ' NutProbeList: mutable list[string] = []', | ||
| ' description: "Test-only External var: a List value set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| // The string-on-wire scalar types (Date/DateTime/Money/Ref) declare no default value, | ||
| // unlike the string/boolean/number/object/list vars above. | ||
| ' NutProbeDate: mutable date', | ||
| ' description: "Test-only External var: a Date value set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| ' NutProbeDateTime: mutable datetime', | ||
| ' description: "Test-only External var: a DateTime value set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| ' NutProbeMoney: mutable currency', | ||
| ' description: "Test-only External var: a Money value set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| ' NutProbeRef: mutable id', | ||
| ' description: "Test-only External var: a Ref (record id) value set via a preview context variable."', | ||
| ' visibility: "External"', | ||
| ].join('\n') | ||
| ); | ||
| writeFileSync(join(aabDir, 'Test_AAB_Preview.agent'), updatedAgentScriptFile); | ||
| }); | ||
|
|
||
|
|
@@ -506,6 +545,115 @@ describe('agent NUTs', () => { | |
| expect(allTracesJson).to.contain(overrideValue, 'expected override value to appear in at least one trace'); | ||
| }); | ||
|
|
||
| it('should round-trip every context-variable wire type into the live preview session trace', async () => { | ||
| const agent = await Agent.init({ connection, project, aabName: bundleApiName }); | ||
| agent.preview.setMockMode('Live Test'); | ||
|
|
||
| // Distinctive sentinels per wire type so a substring match can't collide across types. | ||
| const textValue = 'SDKCV-TEXT-7f3a9b'; | ||
| const numberValue = 8_675_309; | ||
| const jsonTag = 'SDKCV-JSON-4d21c8'; | ||
| const listElement = 'SDKCV-LIST-b58c1e'; | ||
|
|
||
| // External mutable vars are set by their bare name (not the $Context. prefix that linked | ||
| // vars use). Each type maps to its own Variable union member on the wire. | ||
| await agent.preview.start({ | ||
| contextVariables: [ | ||
| { name: 'NutProbeText', type: 'Text', value: textValue }, | ||
| { name: 'NutProbeNum', type: 'Number', value: numberValue }, | ||
| { name: 'NutProbeBool', type: 'Boolean', value: true }, | ||
| { name: 'NutProbeObj', type: 'Json', value: { tag: jsonTag } }, | ||
| { name: 'NutProbeList', type: 'List', value: [listElement] }, | ||
| ], | ||
| }); | ||
|
|
||
| await agent.preview.send('hello'); | ||
|
|
||
| const traces = await agent.preview.getAllTraces(); | ||
| expect(traces).to.be.an('array'); | ||
| expect(traces.length).to.be.greaterThan(0, 'expected at least one trace from the planner'); | ||
|
|
||
| const allTracesJson = traces.map((t) => JSON.stringify(t)).join('\n'); | ||
| expect(allTracesJson).to.contain(textValue, 'Text value should reach the session'); | ||
| expect(allTracesJson).to.contain(String(numberValue), 'Number value should reach the session'); | ||
| expect(allTracesJson).to.contain(jsonTag, 'Json object value should reach the session'); | ||
| expect(allTracesJson).to.contain(listElement, 'List element should reach the session'); | ||
| expect(allTracesJson).to.contain('NutProbeBool', 'Boolean variable should be present in the session'); | ||
| }); | ||
|
|
||
| it('should round-trip the string-on-wire scalar types (Date/DateTime/Money/Ref) into the live preview session trace', async () => { | ||
| const agent = await Agent.init({ connection, project, aabName: bundleApiName }); | ||
| agent.preview.setMockMode('Live Test'); | ||
|
|
||
| // The four string-on-wire scalar API types each serialize as a JSON string. Distinct | ||
| // per-type sentinels keep the substring match from colliding across types. | ||
| const dateValue = '2026-01-15'; | ||
| const dateTimeValue = '2026-01-15T09:00:00Z'; | ||
| const moneyAmount = '42.50'; // Money wire shape is "<ISO> <amount>", e.g. "USD 42.50". | ||
| const refValue = '003000000000002AAA'; | ||
|
|
||
| await agent.preview.start({ | ||
| contextVariables: [ | ||
| { name: 'NutProbeDate', type: 'Date', value: dateValue }, | ||
| { name: 'NutProbeDateTime', type: 'DateTime', value: dateTimeValue }, | ||
| { name: 'NutProbeMoney', type: 'Money', value: `USD ${moneyAmount}` }, | ||
| { name: 'NutProbeRef', type: 'Ref', value: refValue }, | ||
| ], | ||
| }); | ||
|
|
||
| await agent.preview.send('hello'); | ||
|
|
||
| const traces = await agent.preview.getAllTraces(); | ||
| expect(traces).to.be.an('array'); | ||
| expect(traces.length).to.be.greaterThan(0, 'expected at least one trace from the planner'); | ||
|
|
||
| const allTracesJson = traces.map((t) => JSON.stringify(t)).join('\n'); | ||
| expect(allTracesJson).to.contain(dateValue, 'Date value should reach the session'); | ||
| // DateTime is trimmed to minute precision on the wire (seconds/zone dropped), so assert the | ||
| // minute prefix, which is a substring of both the full and the trimmed form. | ||
| expect(allTracesJson).to.contain('2026-01-15T09:00', 'DateTime value should reach the session'); | ||
| // Money display coerces to the org currency, so assert the amount (the invariant part) rather | ||
| // than the ISO prefix we sent. | ||
| expect(allTracesJson).to.contain(moneyAmount, 'Money value should reach the session'); | ||
| expect(allTracesJson).to.contain(refValue, 'Ref value should reach the session'); | ||
| }); | ||
|
|
||
| it('should round-trip structured composition (API Object type and a multi-entry List) into the live preview session trace', async () => { | ||
| const agent = await Agent.init({ connection, project, aabName: bundleApiName }); | ||
| agent.preview.setMockMode('Live Test'); | ||
|
|
||
| // API `Object` (an array of nested typed vars) is distinct from `Json` ({...}); both target | ||
| // an object-typed agent var. A multi-entry List must carry every element, not just the first. | ||
| const objectFieldValue = 'SDKCV-OBJ-9a1f2e'; | ||
| const listFirst = 'SDKCV-LIST0-3c7d55'; | ||
| const listSecond = 'SDKCV-LIST1-e12b90'; | ||
|
|
||
| await agent.preview.start({ | ||
| contextVariables: [ | ||
| { name: 'NutProbeObj', type: 'Object', value: [{ name: 'name', type: 'Text', value: objectFieldValue }] }, | ||
| { | ||
| name: 'NutProbeList', | ||
| type: 'List', | ||
| value: [ | ||
| { type: 'Text', value: listFirst }, | ||
| { type: 'Text', value: listSecond }, | ||
| ], | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await agent.preview.send('hello'); | ||
|
|
||
| const traces = await agent.preview.getAllTraces(); | ||
| expect(traces).to.be.an('array'); | ||
| expect(traces.length).to.be.greaterThan(0, 'expected at least one trace from the planner'); | ||
|
|
||
| const allTracesJson = traces.map((t) => JSON.stringify(t)).join('\n'); | ||
| expect(allTracesJson).to.contain(objectFieldValue, 'nested Object field value should reach the session'); | ||
| expect(allTracesJson).to.contain(listFirst, 'first List element should reach the session'); | ||
| expect(allTracesJson).to.contain(listSecond, 'second List element should reach the session'); | ||
| }); | ||
|
|
||
| it('should end the preview session', async () => { | ||
| const agent = await Agent.init({ connection, project, aabName: bundleApiName }); | ||
| const previewSession = await agent.preview.start(); | ||
|
|
@@ -709,6 +857,11 @@ describe('agent NUTs', () => { | |
| saveAgent: true, | ||
| agentSettings: { | ||
| agentName: legacyAgentName, | ||
| // Reuse the Bot User already provisioned in the describe `before` (and committed via | ||
| // waitForPermSetAssignment) instead of letting core auto-create one in the same | ||
| // transaction, which intermittently races the pre-save validation trigger and fails | ||
| // with "User doesn't have access to agent". Mirrors the sibling spec-create test. | ||
| userId: botUserId, | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should alleviate user provisioning race condition we are hitting on core for these e2e tests. |
||
| }, | ||
| generationInfo: { | ||
| defaultInfo: { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This old code was not in parity with the actual preview API contract.