Skip to content

Commit

Permalink
[JS] chore: MemoryFork and TestMemoryFork unit tests (#1051)
Browse files Browse the repository at this point in the history
## Linked issues

closes: #1052  

## Details

Unit tests added for:
(100% coverage on % statements, branch, funcs, and lines).

- [x] `MemoryFork.ts`
- [x] `TestMemoryFork.ts`


## Attestation Checklist

- [x] My code follows the style guidelines of this project

- I have checked for/fixed spelling, linting, and other errors
- I have commented my code for clarity
- I have made corresponding changes to the documentation (we use
[TypeDoc](https://typedoc.org/) to document our code)
- My changes generate no new warnings
- I have added tests that validates my changes, and provides sufficient
test coverage. I have tested with:
  - Local testing
  - E2E testing in Teams
- New and existing unit tests pass locally with my changes
  • Loading branch information
lilyydu authored Dec 12, 2023
1 parent 8d220c9 commit 61490e5
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 6 deletions.
181 changes: 181 additions & 0 deletions js/packages/teams-ai/src/MemoryFork.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import assert from 'assert';
import { MemoryFork } from './MemoryFork';
import { TestMemoryFork } from './TestMemoryFork';

describe('MemoryFork', () => {
let mockMemory: MemoryFork;
let testMemoryFork: TestMemoryFork;

beforeEach(() => {
testMemoryFork = new TestMemoryFork();
testMemoryFork.setValue('User.userId', '987');
mockMemory = new MemoryFork(testMemoryFork);
});

it('should throw invalid state path error due to many substrings', () => {
const path = 'random.random.conversationId';
assert.throws(() => mockMemory.deleteValue(path), new Error(`Invalid state path: ${path}`));
});

describe('setValue', () => {
it('should assign a value to memory, where scope does not yet exist', () => {
const path = 'Conversation.conversationId';
mockMemory.setValue(path, '123');
assert.equal(mockMemory.hasValue(path), true);
assert.equal(mockMemory.getValue(path), '123');
});

it('should assign a value to memory, where scope already exists', () => {
const pathOne = 'Conversation.conversationId';
mockMemory.setValue(pathOne, '123');
const pathTwo = 'Conversation.userId';
mockMemory.setValue(pathTwo, '432');
assert.equal(mockMemory.hasValue(pathOne), true);
assert.equal(mockMemory.hasValue(pathTwo), true);
assert.equal(mockMemory.getValue(pathOne), '123');
assert.equal(mockMemory.getValue(pathTwo), '432');
});
});

describe('getValue', () => {
it('should retrieve existing value from forked memory', () => {
const path = 'Conversation.conversationId';
mockMemory.setValue(path, '123');
assert.equal(mockMemory.getValue(path), '123');
});

it('should retrieve value from original memory, where forked memory does not contain specified scope', () => {
const path = 'User.userId';
assert.equal(mockMemory.getValue(path), '987');
});

it('should retrieve value from original memory, where forked memory does not have specified name', () => {
const pathOne = 'User.tokenId';
mockMemory.setValue(pathOne, '432');
const pathTwo = 'User.userId';
assert.equal(mockMemory.getValue(pathTwo), '987');
});

it('should retrieve value from original memory, where value exists in both original and forked', () => {
const path = 'User.userId';
mockMemory.setValue(path, '567');
assert.equal(mockMemory.getValue(path), '567');
});

it('should return null as no value exists in forked and original memory', () => {
const path = 'Conversation.tokenId';
assert.equal(mockMemory.getValue(path), null);
});
});

describe('hasValue', () => {
it('should check value from original memory, where forked memory does not contain specified scope', () => {
const path = 'User.userId';
assert.equal(mockMemory.hasValue(path), true);
});

it('should check value from original memory, where forked memory does not have specified name', () => {
const pathOne = 'Conversation.conversationId';
mockMemory.setValue(pathOne, '123');
const pathTwo = 'Conversation.tokenId';
assert.equal(mockMemory.hasValue(pathTwo), false);
});

it('should check non-existing value from original memory, where scope exists', () => {
const path = 'User.tokenId';
assert.equal(mockMemory.hasValue(path), false);
});

it('should check non-existing value from original memory, where scope does not exist', () => {
const path = 'Conversation.tokenId';
assert.equal(mockMemory.hasValue(path), false);
});

it('should perform a check using defaulted temp scope', () => {
const path = 'conversationId';
assert.equal(mockMemory.hasValue(path), false);
});
});

describe('deleteValue', () => {
it('should delete the value from forked memory', () => {
const path = 'Conversation.conversationId';
mockMemory.setValue(path, '123');
mockMemory.deleteValue(path);
assert.equal(mockMemory.hasValue(path), false);
});

it('should delete the defaulted temp scope and name value from forked memory', () => {
const path = 'conversationId';
mockMemory.setValue(path, '123');
mockMemory.deleteValue(path);
assert.equal(mockMemory.hasValue(path), false);
});

it('should delete and check a non-existing value from forked memory, where scope does not exist', () => {
const pathTwo = 'User.conversationId';
mockMemory.deleteValue(pathTwo);
assert.equal(mockMemory.hasValue(pathTwo), false);
});

it('should delete and check a non-existing value from forked memory, where name does not exist', () => {
const pathOne = 'Conversation.conversationId';
mockMemory.setValue(pathOne, '123');
const pathTwo = 'Conversation.tokenId';
mockMemory.deleteValue(pathTwo);
assert.equal(mockMemory.hasValue(pathTwo), false);
});
});
});

describe('TestMemoryFork', () => {
let testMemoryFork: TestMemoryFork;

beforeEach(() => {
testMemoryFork = new TestMemoryFork();
testMemoryFork.setValue('User.userId', '123');
});

it('should throw invalid state path error due to many substrings', () => {
const path = 'random.random.conversationId';
assert.throws(() => testMemoryFork.deleteValue(path), new Error(`Invalid state path: ${path}`));
});

it('should assign a new value where scope already exists', () => {
const path = 'User.conversationId';
testMemoryFork.setValue(path, '987');
assert.equal(testMemoryFork.hasValue(path), true);
});

it('should retrieve an existing value', () => {
const path = 'User.userId';
assert.equal(testMemoryFork.getValue(path), '123');
});

it('should return null for non-existing value', () => {
const path = 'User.tokenId';
assert.equal(testMemoryFork.getValue(path), null);
});

it('should return true for checking an existing value', () => {
const path = 'User.userId';
assert.equal(testMemoryFork.hasValue(path), true);
});

it('should return false for checking a non-existent value, where name does not exist', () => {
const path = 'User.tokenId';
assert.equal(testMemoryFork.hasValue(path), false);
});

it('should check and delete an existing value', () => {
const path = 'User.userId';
testMemoryFork.deleteValue(path);
assert.equal(testMemoryFork.hasValue(path), false);
});

it('should check and delete a non-existing value', () => {
const path = 'Temp.userId';
testMemoryFork.deleteValue(path);
assert.equal(testMemoryFork.hasValue(path), false);
});
});
12 changes: 6 additions & 6 deletions js/packages/teams-ai/src/MemoryFork.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/**
* @module teams-ai
*/
Expand Down Expand Up @@ -73,7 +72,7 @@ export class MemoryFork implements Memory {
* @param path Path to the value to delete in the form of `[scope].property`. If scope is omitted, the value is deleted from the temporary scope.
*/
public deleteValue(path: string): void {
const {scope, name} = this.getScopeAndName(path);
const { scope, name } = this.getScopeAndName(path);
if (this._fork.hasOwnProperty(scope) && this._fork[scope].hasOwnProperty(name)) {
delete this._fork[scope][name];
}
Expand All @@ -87,7 +86,7 @@ export class MemoryFork implements Memory {
* @returns True if the value exists, false otherwise.
*/
public hasValue(path: string): boolean {
const {scope, name} = this.getScopeAndName(path);
const { scope, name } = this.getScopeAndName(path);
if (this._fork.hasOwnProperty(scope)) {
return this._fork[scope].hasOwnProperty(name);
} else {
Expand All @@ -103,7 +102,7 @@ export class MemoryFork implements Memory {
* @returns The value or undefined if not found.
*/
public getValue<TValue = unknown>(path: string): TValue {
const {scope, name} = this.getScopeAndName(path);
const { scope, name } = this.getScopeAndName(path);
if (this._fork.hasOwnProperty(scope)) {
if (this._fork[scope].hasOwnProperty(name)) {
return this._fork[scope][name] as TValue;
Expand All @@ -121,7 +120,7 @@ export class MemoryFork implements Memory {
* @param value Value to assign.
*/
public setValue(path: string, value: unknown): void {
const {scope, name} = this.getScopeAndName(path);
const { scope, name } = this.getScopeAndName(path);
if (!this._fork.hasOwnProperty(scope)) {
this._fork[scope] = {};
}
Expand All @@ -130,6 +129,7 @@ export class MemoryFork implements Memory {
}

/**
* @param path Path to the value to check in the form of `[scope].property`. If scope is omitted, the value is checked in the temporary scope.
* @private
*/
private getScopeAndName(path: string): { scope: string; name: string } {
Expand All @@ -143,4 +143,4 @@ export class MemoryFork implements Memory {

return { scope: parts[0], name: parts[1] };
}
}
}
82 changes: 82 additions & 0 deletions js/packages/teams-ai/src/TestMemoryFork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Memory } from './MemoryFork';

/**
* @private
*/
const TEMP_SCOPE = 'temp';

/**
* A test version of the Memory class used by unit tests.
*/
export class TestMemoryFork implements Memory {
private readonly _fork: Record<string, Record<string, unknown>> = {};

/**
* Deletes the value from the original memory.
* @param path Path to the value to check in the form of `[scope].property`. If scope is omitted, the value is checked in the temporary scope.
*/
public deleteValue(path: string): void {
const { scope, name } = this.getScopeAndName(path);
if (this._fork.hasOwnProperty(scope) && this._fork[scope].hasOwnProperty(name)) {
delete this._fork[scope][name];
}
}

/**
* Checks if the value exists in the original memory.
* @param path Path to the value to check in the form of `[scope].property`. If scope is omitted, the value is checked in the temporary scope.
*/
public hasValue(path: string): boolean {
const { scope, name } = this.getScopeAndName(path);
if (this._fork.hasOwnProperty(scope)) {
return this._fork[scope].hasOwnProperty(name);
}

return false;
}

/**
* Retrieves the value from the original memory. Otherwise, returns null if value does not exist.
* @param path Path to the value to check in the form of `[scope].property`. If scope is omitted, the value is checked in the temporary scope.
*/
public getValue<TValue = unknown>(path: string): TValue {
const { scope, name } = this.getScopeAndName(path);
if (this._fork.hasOwnProperty(scope)) {
if (this._fork[scope].hasOwnProperty(name)) {
return this._fork[scope][name] as TValue;
}
}

return null as TValue;
}

/**
* Sets the value in the original memory.
* @param path Path to the value to check in the form of `[scope].property`. If scope is omitted, the value is checked in the temporary scope.
* @param value Value to assign.
*/
public setValue(path: string, value: unknown): void {
const { scope, name } = this.getScopeAndName(path);
if (!this._fork.hasOwnProperty(scope)) {
this._fork[scope] = {};
}

this._fork[scope][name] = value;
}

/**
* @param path Path to the value to check in the form of `[scope].property`. If scope is omitted, the value is checked in the temporary scope.
* @private
*/
private getScopeAndName(path: string): { scope: string; name: string } {
// Get variable scope and name
const parts = path.split('.');
if (parts.length > 2) {
throw new Error(`Invalid state path: ${path}`);
} else if (parts.length === 1) {
parts.unshift(TEMP_SCOPE);
}

return { scope: parts[0], name: parts[1] };
}
}

0 comments on commit 61490e5

Please sign in to comment.