chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
@@ -0,0 +1,97 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`test Key Path Expression V2 > init const test_key_path = source.a 1`] = `
{
"initializer": {
"keyPath": [
"source",
"a",
],
"kind": "KeyPathExpression",
},
"key": "test_key_path",
"kind": "VariableDeclaration",
"order": 0,
"type": {
"kind": "String",
},
}
`;
exports[`test Key Path Expression V2 > subscribe variable changes by expression 1`] = `
[
1,
{
"kind": "Boolean",
},
]
`;
exports[`test Key Path Expression V2 > subscribe variable changes by expression 2`] = `
[
2,
{
"kind": "Number",
},
]
`;
exports[`test Key Path Expression V2 > subscribe variable changes by expression 3`] = `
[
3,
undefined,
]
`;
exports[`test Key Path Expression V2 > subscribe variable changes by expression 4`] = `
[
4,
{
"kind": "Number",
},
]
`;
exports[`test Key Path Expression V2 > subscribe variable changes by expression 5`] = `
[
5,
undefined,
]
`;
exports[`test Key Path Expression V2 > subscribe variable changes by expression 6`] = `
[
6,
{
"kind": "Number",
},
]
`;
exports[`test Key Path Expression V2 > subscribe variable changes by expression 7`] = `
{
"initializer": {
"enumerateFor": {
"keyPath": [
"source",
"b",
],
"kind": "KeyPathExpression",
},
"kind": "EnumerateExpression",
},
"key": "test_key_path",
"kind": "VariableDeclaration",
"order": 0,
"type": {
"kind": "Number",
},
}
`;
exports[`test Key Path Expression V2 > subscribe variable changes by expression 8`] = `
[
7,
undefined,
]
`;
@@ -0,0 +1,170 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`test Basic Variable Declaration > test globalVariableTable variables 1`] = `
[
"string",
"boolean",
"number",
"integer",
"object",
"map",
]
`;
exports[`test Basic Variable Declaration > test remove variable, update variable, add variable by from a new json 1`] = `
[
"object",
"new_integer",
]
`;
exports[`test Basic Variable Declaration > test remove variable, update variable, add variable by from a new json 2`] = `[]`;
exports[`test Basic Variable Declaration > test simple variable declarations 1`] = `
{
"declarations": [
{
"key": "string",
"kind": "VariableDeclaration",
"order": 0,
"type": {
"kind": "String",
},
},
{
"key": "boolean",
"kind": "VariableDeclaration",
"order": 1,
"type": {
"kind": "Boolean",
},
},
{
"key": "number",
"kind": "VariableDeclaration",
"order": 2,
"type": {
"kind": "Number",
},
},
{
"key": "integer",
"kind": "VariableDeclaration",
"order": 3,
"type": {
"kind": "Integer",
},
},
{
"key": "object",
"kind": "VariableDeclaration",
"order": 4,
"type": {
"kind": "Object",
"properties": [
{
"key": "key1",
"kind": "Property",
"type": {
"kind": "String",
},
},
{
"key": "key2",
"kind": "Property",
"type": {
"kind": "Object",
"properties": [
{
"key": "key1",
"kind": "Property",
"type": {
"kind": "Number",
},
},
],
},
},
{
"key": "key3",
"kind": "Property",
"type": {
"kind": "Array",
},
},
{
"key": "key4",
"kind": "Property",
"type": {
"items": {
"kind": "Object",
"properties": [
{
"key": "key1",
"kind": "Property",
"type": {
"kind": "Boolean",
},
},
],
},
"kind": "Array",
},
},
],
},
},
{
"key": "map",
"kind": "VariableDeclaration",
"order": 5,
"type": {
"keyType": {
"kind": "String",
},
"kind": "Map",
"valueType": {
"kind": "Number",
},
},
},
],
"kind": "VariableDeclarationList",
}
`;
exports[`test Basic Variable Declaration > variable declaration subscribe 1`] = `
{
"kind": "Number",
}
`;
exports[`test Basic Variable Declaration > variable declaration subscribe 2`] = `
{
"kind": "Object",
"properties": [
{
"key": "key1",
"kind": "Property",
"type": {
"kind": "String",
},
},
],
}
`;
exports[`test Basic Variable Declaration > variable declaration subscribe 3`] = `
{
"kind": "Object",
"properties": [
{
"key": "key1",
"kind": "Property",
"type": {
"kind": "Number",
},
},
],
}
`;
@@ -0,0 +1,97 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`test Variable With Initializer > init const test_key_path = source.a 1`] = `
{
"initializer": {
"keyPath": [
"source",
"a",
],
"kind": "KeyPathExpression",
},
"key": "test_key_path",
"kind": "VariableDeclaration",
"order": 0,
"type": {
"kind": "String",
},
}
`;
exports[`test Variable With Initializer > subscribe variable changes by expression 1`] = `
[
1,
{
"kind": "Boolean",
},
]
`;
exports[`test Variable With Initializer > subscribe variable changes by expression 2`] = `
[
2,
{
"kind": "Number",
},
]
`;
exports[`test Variable With Initializer > subscribe variable changes by expression 3`] = `
[
3,
undefined,
]
`;
exports[`test Variable With Initializer > subscribe variable changes by expression 4`] = `
[
4,
{
"kind": "Number",
},
]
`;
exports[`test Variable With Initializer > subscribe variable changes by expression 5`] = `
[
5,
undefined,
]
`;
exports[`test Variable With Initializer > subscribe variable changes by expression 6`] = `
[
6,
{
"kind": "Number",
},
]
`;
exports[`test Variable With Initializer > subscribe variable changes by expression 7`] = `
{
"initializer": {
"enumerateFor": {
"keyPath": [
"source",
"b",
],
"kind": "KeyPathExpression",
},
"kind": "EnumerateExpression",
},
"key": "test_key_path",
"kind": "VariableDeclaration",
"order": 0,
"type": {
"kind": "Number",
},
}
`;
exports[`test Variable With Initializer > subscribe variable changes by expression 8`] = `
[
7,
undefined,
]
`;
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { describe, expect, test } from 'vitest';
import { VariableEngine } from '../../src/variable-engine';
import { ASTNode, postConstructAST } from '../../src/ast';
import { getContainer } from '../../__mocks__/container';
describe('test ast decorators', () => {
const container = getContainer();
const variableEngine: VariableEngine = container.get(VariableEngine);
const testScope = variableEngine.createScope('test');
test('@postConstructAST()', () => {
let postConstructCalled = false;
class PostConstructTest extends ASTNode {
static kind = 'PostConstructTest';
testFlag = false;
@postConstructAST()
init() {
postConstructCalled = true;
this.testFlag = true;
}
fromJSON(json: any): void {
// do nothing
}
toJSON() {
return {
testFlag: this.testFlag,
};
}
}
variableEngine.astRegisters.registerAST(PostConstructTest);
const ast: PostConstructTest = testScope.ast.set('test', {
kind: 'PostConstructTest',
});
const ast2: PostConstructTest = testScope.ast.set('test', {
kind: 'PostConstructTest',
});
expect(postConstructCalled).toBeTruthy();
expect(ast.testFlag).toBeTruthy();
expect(ast2.testFlag).toBeTruthy();
});
test('@postConstructAST() Annotate Only One time', () => {
expect(() => {
class PostConstructTest2 extends ASTNode {
static kind = 'PostConstructTest2';
testFlag1 = false;
testFlag2 = false;
@postConstructAST()
init() {
this.testFlag1 = true;
}
@postConstructAST()
init2() {
this.testFlag2 = true;
}
fromJSON(json: any): void {
// do nothing
}
toJSON() {
return {
testFlag1: this.testFlag1,
testFlag2: this.testFlag2,
};
}
}
variableEngine.astRegisters.registerAST(PostConstructTest2);
}).toThrowError();
});
});
@@ -0,0 +1,338 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { vi, describe, test, expect } from 'vitest';
import { getParentFields } from '../../src/ast/utils/variable-field';
import { ASTKind, VariableEngine, VariableDeclaration, ASTFactory } from '../../src';
import { getContainer } from '../../__mocks__/container';
const {
createVariableDeclaration,
createObject,
createProperty,
createString,
createNumber,
createKeyPathExpression,
createArray,
createEnumerateExpression,
} = ASTFactory;
vi.mock('nanoid', () => {
let mockId = 0;
return {
nanoid: () => 'mocked-id-' + mockId++,
};
});
/**
* 测试通过表达式生成的变量
*/
describe('test Key Path Expression V2', () => {
const container = getContainer();
const variableEngine = container.get(VariableEngine);
const globalScope = variableEngine.createScope('global');
const testScope = variableEngine.createScope('test');
const globalTestVariable = globalScope.ast.set<VariableDeclaration>(
'test',
createVariableDeclaration({
key: 'source',
type: createObject({
properties: [
createProperty({
key: 'a',
type: createString(),
}),
createProperty({
key: 'b',
type: createNumber(),
}),
],
}),
})
);
test('init const test_key_path = source.a', () => {
const variableByKeyPath = testScope.ast.set<VariableDeclaration>(
'variableByKeyPath',
createVariableDeclaration({
key: 'test_key_path',
initializer: createKeyPathExpression({
keyPath: ['source', 'a'],
}),
})
)!;
expect(variableByKeyPath.initializer?.kind).toEqual(ASTKind.KeyPathExpression);
expect(variableByKeyPath.initializer?.refs.map((_ref) => _ref?.key)).toEqual(['a']);
// variableByKeyPath 的类型重新建立了实例,其父节点为当前节点
expect(getParentFields(variableByKeyPath.type).map((_field) => _field?.key)).toEqual([
'test_key_path',
]);
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.String });
expect(variableByKeyPath.toJSON()).toMatchSnapshot();
});
test('subscribe variable changes by expression', () => {
// const variableByKeyPath = source.a;
const variableByKeyPath = testScope.output.getVariableByKey('test_key_path')!;
let typeChangeTimes = 0;
variableByKeyPath.onTypeChange((type) => {
typeChangeTimes++;
expect([typeChangeTimes, type?.toJSON()]).toMatchSnapshot();
});
// source.a 发生变化时,则响应更新变量
// source.a -> boolean
globalTestVariable.getByKeyPath(['a'])?.updateType(ASTKind.Boolean);
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Boolean });
expect(typeChangeTimes).toBe(1);
// 改成引用 source.b,响应更新变量
// const variableByKeyPath = source.b;
variableByKeyPath.updateInitializer({
kind: ASTKind.KeyPathExpression,
keyPath: ['source', 'b'],
});
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
expect(typeChangeTimes).toBe(2);
// source.b 被删除,变量类型变为空
globalTestVariable.type.fromJSON({
properties: [
{
key: 'a',
type: ASTKind.String,
},
],
});
expect(variableByKeyPath.type).toBeUndefined();
expect(typeChangeTimes).toBe(3);
// source.b 加回来,变量类型变回来且触发表达式更新
globalTestVariable.type.fromJSON(
createObject({
properties: [
createProperty({ key: 'a', type: createString() }),
createProperty({ key: 'b', type: createNumber() }),
],
})
);
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
expect(typeChangeTimes).toBe(4);
// 改成 EnumerateExpression
variableByKeyPath.updateInitializer(
createEnumerateExpression({
enumerateFor: createKeyPathExpression({
keyPath: ['source', 'b'],
}),
})
);
expect(variableByKeyPath.type).toBeUndefined();
expect(variableByKeyPath.initializer?.refs).toEqual([]);
variableByKeyPath.initializer?.refreshRefs();
expect(variableByKeyPath.initializer?.refs).toEqual([]);
expect(typeChangeTimes).toBe(5);
// 数组下钻,类型也会更新
globalTestVariable.type.fromJSON(
createObject({
properties: [
createProperty({
key: 'b',
type: createArray({
items: createNumber(),
}),
}),
],
})
);
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
expect(typeChangeTimes).toBe(6);
expect(variableByKeyPath.toJSON()).toMatchSnapshot();
// 原作用域删除,显示为空类型
globalScope.dispose();
expect(variableByKeyPath.scope.depScopes.map((_scope) => _scope.id)).toEqual([]);
expect(variableByKeyPath.type).toBeUndefined();
expect(typeChangeTimes).toBe(7);
});
const cycle1 = variableEngine.createScope('cycle1');
const cycle2 = variableEngine.createScope('cycle2');
const cycle3 = variableEngine.createScope('cycle3');
test('cycle scope with normal refs', () => {
const cycle1Var = cycle1.ast.set<VariableDeclaration>(
'var',
createVariableDeclaration({
key: 'cycle1_var',
type: createObject({
properties: [
createProperty({
key: 'a',
type: createArray({
items: createString(),
}),
}),
],
}),
})
);
const cycle2Var = cycle2.ast.set<VariableDeclaration>(
'var',
createVariableDeclaration({
key: 'cycle2_var',
initializer: createEnumerateExpression({
enumerateFor: createKeyPathExpression({
keyPath: ['cycle1_var', 'a'],
}),
}),
})
);
const cycle3Var = cycle3.ast.set<VariableDeclaration>(
'var',
createVariableDeclaration({
key: 'cycle3_var',
initializer: createKeyPathExpression({
keyPath: ['cycle2_var'],
}),
})
);
expect(cycle1Var.type.toJSON()).toEqual({
kind: ASTKind.Object,
properties: [
{
kind: ASTKind.Property,
key: 'a',
type: {
kind: ASTKind.Array,
items: { kind: ASTKind.String },
},
},
],
});
expect(cycle2Var.type.toJSON()).toEqual({
kind: ASTKind.String,
});
expect(cycle3Var.type.toJSON()).toEqual({
kind: ASTKind.String,
});
});
test('cycle scope with child ref', () => {
const cycle1Var = cycle1.ast.get('var')! as VariableDeclaration;
const cycle2Var = cycle2.ast.get('var')! as VariableDeclaration;
const cycle3Var = cycle3.ast.get('var')! as VariableDeclaration;
cycle1Var.fromJSON({
type: createObject({
properties: [
createProperty({
key: 'a',
type: createArray({
items: createString(),
}),
}),
createProperty({
key: 'b',
initializer: createKeyPathExpression({
keyPath: ['cycle3_var'],
}),
}),
],
}),
});
expect(cycle1Var.type.toJSON()).toEqual({
kind: ASTKind.Object,
properties: [
{
kind: ASTKind.Property,
key: 'a',
type: {
kind: ASTKind.Array,
items: { kind: ASTKind.String },
},
},
{
kind: ASTKind.Property,
key: 'b',
initializer: {
kind: ASTKind.KeyPathExpression,
keyPath: ['cycle3_var'],
},
type: {
kind: ASTKind.String,
},
},
],
});
expect(cycle2Var.type.toJSON()).toEqual({
kind: ASTKind.String,
});
expect(cycle3Var.type.toJSON()).toEqual({
kind: ASTKind.String,
});
});
test('cycle scope with cycle ref', () => {
const cycle1Var = cycle1.ast.get('var')! as VariableDeclaration;
const cycle2Var = cycle2.ast.get('var')! as VariableDeclaration;
const cycle3Var = cycle3.ast.get('var')! as VariableDeclaration;
cycle1Var.fromJSON({
type: createObject({
properties: [
createProperty({
key: 'a',
initializer: createKeyPathExpression({
keyPath: ['cycle3_var'],
}),
}),
createProperty({
key: 'b',
initializer: createKeyPathExpression({
keyPath: ['cycle3_var'],
}),
}),
],
}),
});
expect(cycle1Var.type.toJSON()).toEqual({
kind: ASTKind.Object,
properties: [
{
kind: ASTKind.Property,
key: 'a',
initializer: {
kind: ASTKind.KeyPathExpression,
keyPath: ['cycle3_var'],
},
// 发生循环引用时,type 清空
},
{
kind: ASTKind.Property,
key: 'b',
initializer: {
kind: ASTKind.KeyPathExpression,
keyPath: ['cycle3_var'],
},
// 发生循环引用时,type 清空
},
],
});
expect(cycle2Var.type?.toJSON()).toBeUndefined();
expect(cycle3Var.type?.toJSON()).toBeUndefined();
});
});
@@ -0,0 +1,208 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { vi, describe, test, expect } from 'vitest';
import {
ASTKind,
ASTMatch,
ObjectType,
NumberType,
VariableEngine,
VariableDeclaration,
} from '../../src';
import { simpleVariableList } from '../../__mocks__/variables';
import { getContainer } from '../../__mocks__/container';
vi.mock('nanoid', () => {
let mockId = 0;
return {
nanoid: () => 'mocked-id-' + mockId++,
};
});
/**
* 测试基本的变量声明场景
*/
describe('test Basic Variable Declaration', () => {
const container = getContainer();
const variableEngine = container.get(VariableEngine);
const globalVariableTable = variableEngine.globalVariableTable;
const testScope = variableEngine.createScope('test');
test('test simple variable declarations', () => {
const simpleCase = testScope.ast.set('simple case', simpleVariableList);
expect(simpleCase?.toJSON()).toMatchSnapshot();
});
test('test globalVariableTable variables', () => {
expect(globalVariableTable.variables.map((_v) => _v.key)).toMatchSnapshot();
});
test('test get variable by key path', () => {
const undefinedCases = [
['object', 'key2', 'unExisted'],
['unExisted'],
['string', 'drilldownString'],
['object', 'key1', 'drilldownString'],
['object', 'key4', 'unExistedKeyInArray', 'key1'],
];
const availableCases: [string[], string][] = [
[['object', 'key2', 'key1'], ASTKind.Number],
[['object', 'key4', '0', 'key1'], ASTKind.Boolean],
];
availableCases.forEach(([_case, _resType]) => {
expect(globalVariableTable.getByKeyPath(_case)?.type.kind).toEqual(_resType);
});
undefinedCases.forEach((_case) => {
expect(globalVariableTable.getByKeyPath(_case)).toBeUndefined();
});
});
test('test remove variable, update variable, add variable by from a new json', () => {
const previousVariable1 = globalVariableTable.getByKeyPath(['object', 'key2', 'key1'])!;
const previousVariable2 = globalVariableTable.getByKeyPath(['integer'])!;
const previousVariable3 = globalVariableTable.getByKeyPath(['object', 'key4'])!;
testScope.ast.set('simple case', {
kind: ASTKind.VariableDeclarationList,
declarations: [
{
kind: ASTKind.VariableDeclaration,
type: ASTKind.Integer,
key: 'new_integer',
},
{
kind: ASTKind.VariableDeclaration,
key: 'object',
type: {
kind: ASTKind.Object,
properties: [
{
key: 'key2',
type: {
kind: ASTKind.Object,
},
},
{
key: 'key4',
type: ASTKind.String,
},
{
key: 'key5',
type: ASTKind.Array,
},
],
},
},
],
});
expect(previousVariable1.disposed).toBe(true);
expect(previousVariable2.disposed).toBe(true);
expect(previousVariable3.disposed).toBe(false);
expect(previousVariable3.version).toBe(1); // 更新次数为一次
expect(testScope.ast.version).toBe(2); // 调用了两次 fromJSON,因此更新了两次
expect(globalVariableTable.variables.map((_v) => _v.key)).toMatchSnapshot();
expect(globalVariableTable.version).toBe(2 + 1); // 调用了两次 fromJSON + Object 变量的下钻发生变化,因此 version 是 2 + 1
expect(testScope.available.variables.map((_v) => _v.key)).toMatchSnapshot();
});
test('remove variables', () => {
let isOutputChanged = false;
let isAnyVariableChanged = false;
testScope.output.onDataChange(() => {
isOutputChanged = true;
});
testScope.output.onAnyVariableChange(() => {
isAnyVariableChanged = true;
});
// 删除所有变量
testScope.ast.set('simple case', {
kind: ASTKind.VariableDeclarationList,
declarations: [],
});
expect(isOutputChanged).toBeTruthy();
expect(isAnyVariableChanged).toBeFalsy();
});
test('variable declaration subscribe', () => {
const declaration: VariableDeclaration = testScope.ast.set('subscribeTest', {
kind: ASTKind.VariableDeclaration,
type: ASTKind.String,
ui: { label: 'test Label' },
})!;
let declarationChangeTimes = 0;
let typeChangeTimes = 0;
let outputChangeTimes = 0;
let anyVariableChangeTimes = 0;
testScope.output.onDataChange(() => {
outputChangeTimes++;
});
testScope.output.onAnyVariableChange(() => {
anyVariableChangeTimes++;
});
declaration.subscribe(() => {
declarationChangeTimes++;
});
declaration.onTypeChange((type) => {
expect(type?.toJSON()).toMatchSnapshot();
typeChangeTimes++;
});
declaration.fromJSON({
type: ASTKind.String,
meta: { label: 'test New Label' },
});
expect(declarationChangeTimes).toBe(1);
expect(anyVariableChangeTimes).toBe(1);
expect(typeChangeTimes).toBe(0);
expect(outputChangeTimes).toBe(0);
expect(declaration.meta.label).toEqual('test New Label');
declaration.fromJSON({
type: ASTKind.Number,
meta: { label: 'test Label' },
});
expect(ASTMatch.is(declaration.type, NumberType)).toBeTruthy();
expect(declarationChangeTimes).toBe(2);
expect(anyVariableChangeTimes).toBe(2);
expect(typeChangeTimes).toBe(1);
expect(outputChangeTimes).toBe(0);
expect(declaration.meta.label).toEqual('test Label');
declaration.fromJSON({
type: {
kind: ASTKind.Object,
properties: [
{
key: 'key1',
type: ASTKind.String,
},
],
},
meta: { label: 'test Label' },
});
expect(ASTMatch.is(declaration.type, ObjectType)).toBeTruthy();
expect(declarationChangeTimes).toBe(3);
expect(anyVariableChangeTimes).toBe(3);
expect(typeChangeTimes).toBe(2);
expect(outputChangeTimes).toBe(0);
// 变量下钻字段变换类型,变量也会更新
const key1Variable = (declaration.type as ObjectType).getByKeyPath(['key1']);
key1Variable?.updateType(ASTKind.Number);
expect(declarationChangeTimes).toBe(4);
expect(anyVariableChangeTimes).toBe(4);
expect(typeChangeTimes).toBe(3);
expect(outputChangeTimes).toBe(0);
});
});
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { vi, describe, test, expect } from 'vitest';
import {
ASTMatch,
ObjectType,
NumberType,
VariableEngine,
StringType,
VariableDeclarationList,
BooleanType,
IntegerType,
MapType,
ArrayType,
} from '../../src';
import { simpleVariableList } from '../../__mocks__/variables';
import { getContainer } from '../../__mocks__/container';
vi.mock('nanoid', () => {
let mockId = 0;
return {
nanoid: () => 'mocked-id-' + mockId++,
};
});
/**
* 测试基本的变量声明场景
*/
describe('test Basic Variable Declaration', () => {
const container = getContainer();
const variableEngine = container.get(VariableEngine);
const testScope = variableEngine.createScope('test');
test('test simple variable match', () => {
const simpleCase = testScope.ast.set('simple case', simpleVariableList);
if (!ASTMatch.isVariableDeclarationList(simpleCase)) {
throw new Error('simpleCase is not a VariableDeclarationList');
}
expect(ASTMatch.isVariableDeclarationList(simpleCase)).toBeTruthy();
expect(ASTMatch.is(simpleCase, VariableDeclarationList)).toBeTruthy();
const stringDeclaration = simpleCase.declarations[0];
const booleanDeclaration = simpleCase.declarations[1];
const numberDeclaration = simpleCase.declarations[2];
const integerDeclaration = simpleCase.declarations[3];
const objectDeclaration = simpleCase.declarations[4];
const mapDeclaration = simpleCase.declarations[5];
const arrayProperty = testScope.output.globalVariableTable.getByKeyPath(['object', 'key4']);
expect(ASTMatch.isString(stringDeclaration.type)).toBeTruthy();
expect(ASTMatch.is(stringDeclaration.type, StringType)).toBeTruthy();
expect(ASTMatch.isBoolean(booleanDeclaration.type)).toBeTruthy();
expect(ASTMatch.is(booleanDeclaration.type, BooleanType)).toBeTruthy();
expect(ASTMatch.isNumber(numberDeclaration.type)).toBeTruthy();
expect(ASTMatch.is(numberDeclaration.type, NumberType)).toBeTruthy();
expect(ASTMatch.isInteger(integerDeclaration.type)).toBeTruthy();
expect(ASTMatch.is(integerDeclaration.type, IntegerType)).toBeTruthy();
expect(ASTMatch.isObject(objectDeclaration.type)).toBeTruthy();
expect(ASTMatch.is(objectDeclaration.type, ObjectType)).toBeTruthy();
expect(ASTMatch.isMap(mapDeclaration.type)).toBeTruthy();
expect(ASTMatch.is(mapDeclaration.type, MapType)).toBeTruthy();
if (!ASTMatch.isProperty(arrayProperty)) {
throw new Error('arrayProperty is not a Property');
}
expect(ASTMatch.isArray(arrayProperty.type)).toBeTruthy();
expect(ASTMatch.is(arrayProperty.type, ArrayType)).toBeTruthy();
});
});
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { describe, expect, test } from 'vitest';
import { VariableEngine } from '../../src/variable-engine';
import { ASTFactory } from '../../src/ast';
import { simpleVariableList } from '../../__mocks__/variables';
import { getContainer } from '../../__mocks__/container';
describe('Test Variable ThrowError', () => {
const container = getContainer();
const variableEngine = container.get(VariableEngine);
const testScope = variableEngine.createScope('test');
testScope.ast.set('simple case', simpleVariableList);
test('throw error when call getByKeyPath in base type', () => {
const stringVariable = testScope.output.getVariableByKey('string');
expect(() => stringVariable?.type.getByKeyPath(['throw'])).toThrowError();
});
test('not throw error when variable is disposed for twice', () => {
const stringVariable = testScope.output.getVariableByKey('string');
expect(stringVariable?.disposed).toBeFalsy();
// 删除所有变量
testScope.ast.set(
'simple case',
ASTFactory.createVariableDeclarationList({ declarations: [] }),
);
expect(stringVariable?.disposed).toBeTruthy();
stringVariable?.dispose();
expect(stringVariable?.disposed).toBeTruthy();
});
test('not throw error when scope is disposed and fire its events', () => {
const toDisposeScope = variableEngine.createScope('toDisposeTest');
let eventCalledTimes = 0;
toDisposeScope.event.on('test', () => eventCalledTimes++);
toDisposeScope.refreshDeps();
toDisposeScope.event.dispatch({ type: 'test' });
expect(toDisposeScope.disposed).toBeFalsy();
expect(eventCalledTimes).toBe(1);
toDisposeScope.dispose();
expect(toDisposeScope.disposed).toBeTruthy();
toDisposeScope.event.dispatch({ type: 'test' });
toDisposeScope.refreshDeps();
expect(eventCalledTimes).toBe(1);
});
test('not throw error when ast is disposed and fire its events', () => {
const toDisposeAST = testScope.ast.set(
'dispose case',
ASTFactory.createVariableDeclaration({
key: 'toDispose',
type: ASTFactory.createString(),
}),
);
let changeTimes = 0;
toDisposeAST.subscribe(() => changeTimes++);
toDisposeAST.fromJSON({
key: 'toDispose',
type: ASTFactory.createNumber(),
});
expect(changeTimes).toBe(1);
expect(toDisposeAST.disposed).toBeFalsy();
testScope.ast.remove('dispose case');
expect(toDisposeAST.disposed).toBeTruthy();
toDisposeAST.fireChange();
expect(changeTimes).toBe(1);
});
});
@@ -0,0 +1,195 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { describe, expect, test } from 'vitest';
import { VariableEngine } from '../../src/variable-engine';
import { ASTFactory, ASTKind, CustomType, VariableDeclaration } from '../../src/ast';
import { getContainer } from '../../__mocks__/container';
const {
createObject,
createNumber,
createInteger,
createBoolean,
createString,
createArray,
createCustomType,
createMap,
createProperty,
createUnion,
create,
} = ASTFactory;
describe('Test Variable Type Equal', () => {
const container = getContainer();
const variableEngine = container.get(VariableEngine);
const testScope = variableEngine.createScope('test');
const testObject1 = createObject({
properties: [
createProperty({ key: 'a', type: createString() }),
createProperty({ key: 'b', type: createNumber() }),
createProperty({ key: 'c', type: createBoolean() }),
createProperty({ key: 'd', type: createObject({}) }),
createProperty({ key: 'e', type: createArray({}) }),
createProperty({ key: 'f', type: createMap({}) }),
createProperty({ key: 'g', type: createCustomType({ typeName: 'Custom' }) }),
],
});
const testObject2 = createObject({
properties: [
createProperty({ key: 'a', type: createString() }),
createProperty({ key: 'b', type: createInteger() }),
createProperty({ key: 'c', type: createBoolean() }),
],
});
const testObject3 = createObject({
properties: [
createProperty({ key: 'a', type: createString() }),
createProperty({ key: 'b', type: testObject2 }),
createProperty({
key: 'c',
type: createArray({
items: testObject2,
}),
}),
createProperty({
key: 'd',
type: createArray({ items: createString() }),
}),
createProperty({
key: 'e',
type: createMap({
valueType: createNumber(),
}),
}),
createProperty({
key: 'f',
type: createMap({
valueType: createString(),
}),
}),
],
});
const variable1: VariableDeclaration = testScope.ast.set('variable1', {
kind: ASTKind.VariableDeclaration,
key: 'variable1',
type: testObject1,
});
const variable2: VariableDeclaration = testScope.ast.set('variable2', {
kind: ASTKind.VariableDeclaration,
key: 'variable2',
type: testObject2,
});
const variable3: VariableDeclaration = testScope.ast.set('variable3', {
kind: ASTKind.VariableDeclaration,
key: 'variable3',
type: testObject3,
});
test('Test Simple Equal', () => {
expect(variable1.type.isTypeEqual(testObject1)).toBeTruthy();
expect(variable2.type.isTypeEqual(testObject2)).toBeTruthy();
expect(variable3.type.isTypeEqual(testObject3)).toBeTruthy();
expect(variable1.type.isTypeEqual(testObject2)).toBeFalsy();
expect(variable2.type.isTypeEqual(testObject3)).toBeFalsy();
});
test('Test Union Type Equal', () => {
expect(
variable1.type.isTypeEqual(
createUnion({
types: [testObject1, testObject2, testObject3],
})
)
).toBeTruthy();
expect(
variable2.type.isTypeEqual(
createUnion({
types: [testObject1, testObject2, testObject3],
})
)
).toBeTruthy();
expect(
variable3.type.isTypeEqual(
createUnion({
types: [testObject1, testObject2, testObject3],
})
)
).toBeTruthy();
expect(variable1.type.isTypeEqual({ kind: ASTKind.Union })).toBeFalsy();
});
test('Test Union Type Equal In Child Properties', () => {
const UnionBasicTypes = createUnion({
types: [ASTKind.String, ASTKind.Number, ASTKind.Boolean],
});
expect(
variable2.type.isTypeEqual({
kind: ASTKind.Object,
properties: [
{
key: 'a',
type: UnionBasicTypes,
},
{
key: 'b',
type: UnionBasicTypes,
},
{
key: 'c',
type: UnionBasicTypes,
},
],
})
);
});
test('Test Weak Compare', () => {
expect(variable1.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();
expect(variable2.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();
expect(variable3.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();
expect(
variable3.getByKeyPath(['c'])?.type.isTypeEqual({ kind: ASTKind.Array, weak: true })
).toBeTruthy();
expect(
variable3.getByKeyPath(['d'])?.type.isTypeEqual({ kind: ASTKind.Array, weak: true })
).toBeTruthy();
expect(
variable3.getByKeyPath(['e'])?.type.isTypeEqual({ kind: ASTKind.Map, weak: true })
).toBeTruthy();
expect(
variable3.getByKeyPath(['f'])?.type.isTypeEqual({ kind: ASTKind.Map, weak: true })
).toBeTruthy();
});
test('CustomType Equal', () => {
const customType1 = createCustomType({ typeName: 'Custom' });
const customType2 = createCustomType({ typeName: 'Custom2' });
const customType3 = create(CustomType, { typeName: 'Custom' });
const unionCustomTypes = createUnion({
types: [customType1, customType2],
});
const customTypeProperty = variable1.getByKeyPath(['g'])?.type;
expect(customTypeProperty?.isTypeEqual(customType1)).toBeTruthy();
expect(customTypeProperty?.isTypeEqual(customType2)).toBeFalsy();
expect(customTypeProperty?.isTypeEqual(customType3)).toBeTruthy();
expect(customTypeProperty?.isTypeEqual(unionCustomTypes)).toBeTruthy();
});
});
@@ -0,0 +1,168 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { vi, describe, test, expect } from 'vitest';
import { ASTKind, VariableEngine, VariableDeclaration, ASTFactory } from '../../src';
import { getContainer } from '../../__mocks__/container';
const {
createVariableDeclaration,
createObject,
createProperty,
createString,
createNumber,
createKeyPathExpression,
createArray,
createEnumerateExpression,
} = ASTFactory;
vi.mock('nanoid', () => {
let mockId = 0;
return {
nanoid: () => 'mocked-id-' + mockId++,
};
});
/**
* 测试通过表达式生成的变量
*/
describe('test Variable With Initializer', () => {
const container = getContainer();
const variableEngine = container.get(VariableEngine);
const globalScope = variableEngine.createScope('global');
const testScope = variableEngine.createScope('test');
const globalTestVariable = globalScope.ast.set<VariableDeclaration>(
'test',
createVariableDeclaration({
key: 'source',
type: createObject({
properties: [
createProperty({
key: 'a',
type: createString(),
}),
createProperty({
key: 'b',
type: createNumber(),
}),
],
}),
}),
);
test('test depScopes', () => {
expect(testScope.depScopes.map(_scope => _scope.id)).toEqual(['global']);
});
test('init const test_key_path = source.a', () => {
const variableByKeyPath = testScope.ast.set<VariableDeclaration>(
'variableByKeyPath',
createVariableDeclaration({
key: 'test_key_path',
initializer: createKeyPathExpression({
keyPath: ['source', 'a'],
}),
}),
)!;
expect(variableByKeyPath.initializer?.kind).toEqual(ASTKind.KeyPathExpression);
expect(variableByKeyPath.initializer?.refs.map(_ref => _ref?.key)).toEqual(['a']);
expect(variableByKeyPath.initializer?.parentFields.map(_field => _field?.key)).toEqual([
'test_key_path',
]);
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.String });
expect(variableByKeyPath.toJSON()).toMatchSnapshot();
});
test('subscribe variable changes by expression', () => {
// const variableByKeyPath = source.a;
const variableByKeyPath = testScope.output.getVariableByKey('test_key_path')!;
let typeChangeTimes = 0;
variableByKeyPath.onTypeChange(type => {
typeChangeTimes++;
expect([typeChangeTimes, type?.toJSON()]).toMatchSnapshot();
});
// source.a 发生变化时,则响应更新变量
// source.a -> boolean
globalTestVariable.getByKeyPath(['a'])?.updateType(ASTKind.Boolean);
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Boolean });
expect(typeChangeTimes).toBe(1);
// 改成引用 source.b,响应更新变量
// const variableByKeyPath = source.b;
variableByKeyPath.updateInitializer({
kind: ASTKind.KeyPathExpression,
keyPath: ['source', 'b'],
});
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
expect(typeChangeTimes).toBe(2);
// source.b 被删除,变量类型变为空
globalTestVariable.type.fromJSON({
properties: [
{
key: 'a',
type: ASTKind.String,
},
],
});
expect(variableByKeyPath.type).toBeUndefined();
expect(typeChangeTimes).toBe(3);
// source.b 加回来,变量类型变回来且触发表达式更新
globalTestVariable.type.fromJSON(
createObject({
properties: [
createProperty({ key: 'a', type: createString() }),
createProperty({ key: 'b', type: createNumber() }),
],
}),
);
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
expect(typeChangeTimes).toBe(4);
// 改成 EnumerateExpression
variableByKeyPath.updateInitializer(
createEnumerateExpression({
enumerateFor: createKeyPathExpression({
keyPath: ['source', 'b'],
}),
}),
);
expect(variableByKeyPath.type).toBeUndefined();
expect(variableByKeyPath.initializer?.refs).toEqual([]);
variableByKeyPath.initializer?.refreshRefs();
expect(variableByKeyPath.initializer?.refs).toEqual([]);
expect(typeChangeTimes).toBe(5);
// 数组下钻,类型也会更新
globalTestVariable.type.fromJSON(
createObject({
properties: [
createProperty({
key: 'b',
type: createArray({
items: createNumber(),
}),
}),
],
}),
);
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
expect(typeChangeTimes).toBe(6);
expect(variableByKeyPath.toJSON()).toMatchSnapshot();
// 原作用域删除,显示为空类型
globalScope.dispose();
expect(variableByKeyPath.scope.depScopes.map(_scope => _scope.id)).toEqual([]);
expect(variableByKeyPath.type).toBeUndefined();
expect(typeChangeTimes).toBe(7);
});
});
@@ -0,0 +1,179 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { describe, expect, test } from 'vitest';
import { isEqual } from 'lodash-es';
import { Container, injectable } from 'inversify';
import { Emitter } from '@flowgram.ai/utils';
import { CreateASTParams } from '../../src/ast/types';
import {
ASTFactory,
BaseExpression,
BaseType,
VariableDeclaration,
VariableEngine,
injectToAST,
} from '../../src';
import { getContainer } from '../../__mocks__/container';
interface PyExpressionJSON {
content: string;
uri: string;
}
const delay = (timeout: number) => new Promise((resolve) => setTimeout(resolve, timeout));
describe('Case Run Down: Python Expression In Blockwise', () => {
@injectable()
class PythonService {
// 模拟 Blockwise 表达式后端存储推导的信息
uriToType: Record<string, any> = {
'blockwise://expression/a': { kind: 'String' },
'blockwise://expression/b': { kind: 'Number' },
};
// 模拟 Blockwise 单表达式推导函数
async infer(json: PyExpressionJSON) {
return Promise.resolve(this.uriToType[json.uri]);
}
// 模拟表达式后端重新推导表达式
batchInferEmitter = new Emitter<void>();
onBatchInfer = this.batchInferEmitter.event;
}
class PythonExpression extends BaseExpression<PyExpressionJSON> {
@injectToAST(PythonService) declare service: PythonService;
static kind: string = 'BlockwisePythonExpression';
_uri?: string;
_content?: string;
// Blockwise 通过 schema 变更时输出的方式来进行类型推导
getRefFields() {
return [];
}
returnType: BaseType<any> | undefined;
_prevType: any;
// Blockwise 中 表达式通过 uri 来进行索引定位
fromJSON(json: PyExpressionJSON): void {
if (json.uri !== this._uri || json.content !== this._content) {
this.service.infer(json).then((res) => {
this._prevType = res;
this.updateChildNodeByKey('returnType', res);
});
this._uri = json.uri;
this._content = json.content;
}
}
toJSON(): PyExpressionJSON {
return {
content: this._content!,
uri: this._uri!,
};
}
constructor(params: CreateASTParams) {
super(params);
this.toDispose.push(
// 监听后端触发批量校验
this.service.onBatchInfer(() => {
const nextType = this.service.uriToType[this._uri!];
if (!isEqual(nextType, this._prevType)) {
this.updateChildNodeByKey('returnType', nextType);
this._prevType = nextType;
}
})
);
}
}
const createBlockwisePythonExpression = (json: PyExpressionJSON) => ({
kind: 'BlockwisePythonExpression',
...json,
});
const container: Container = getContainer((bind) => {
bind(PythonService).toSelf().inSingletonScope();
});
const variableEngine: VariableEngine = container.get(VariableEngine);
const pythonService: PythonService = container.get(PythonService);
variableEngine.astRegisters.registerAST(PythonExpression, () => ({
pythonService,
}));
const testScope = variableEngine.createScope('test');
test('1. Infer When Expression Changed', async () => {
const variable1: VariableDeclaration = testScope.ast.set(
'variable1',
ASTFactory.createVariableDeclaration({
key: 'variable1',
initializer: createBlockwisePythonExpression({
uri: 'blockwise://expression/a',
content: 'a + b',
}),
})
);
await delay(0);
expect(variable1.type?.kind).toEqual('String');
expect(variable1.version).toEqual(1);
// 更新表达式
pythonService.uriToType['blockwise://expression/a'] = { kind: 'Number' };
variable1.fromJSON({
initializer: createBlockwisePythonExpression({
uri: 'blockwise://expression/a',
content: 'a + b + c',
}),
});
await delay(0);
expect(variable1.type?.kind).toEqual('Number');
expect(variable1.version).toEqual(2);
});
test('2. Infer When Global Update Triggered', async () => {
const variable2: VariableDeclaration = testScope.ast.set(
'variable2',
ASTFactory.createVariableDeclaration({
key: 'variable2',
initializer: createBlockwisePythonExpression({
uri: 'blockwise://expression/b',
content: 'a + b',
}),
})
);
await delay(0);
expect(variable2.type?.kind).toEqual('Number');
expect(variable2.version).toEqual(1);
// 1. 表达式类型没有变化
pythonService.uriToType['blockwise://expression/b'] = { kind: 'Number' };
pythonService.batchInferEmitter.fire();
await delay(0);
expect(variable2.type?.kind).toEqual('Number');
expect(variable2.version).toEqual(1);
// 2. 表达式类型发生了变化
pythonService.uriToType['blockwise://expression/b'] = { kind: 'Boolean' };
pythonService.batchInferEmitter.fire();
await delay(0);
expect(variable2.type?.kind).toEqual('Boolean');
expect(variable2.version).toEqual(2);
});
});
@@ -0,0 +1,146 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { vi, describe, test, beforeEach, expect } from 'vitest';
import { cloneDeep } from 'lodash-es';
import { ASTNode, ASTNodeFlags, VariableEngine, VariableFieldKeyRenameService } from '../../src';
import { simpleVariableList } from '../../__mocks__/variables';
import { getContainer } from '../../__mocks__/container';
vi.mock('nanoid', () => {
let mockId = 0;
return {
nanoid: () => 'mocked-id-' + mockId++,
};
});
function getFieldKeys(node: ASTNode): string[] {
let _curr = node?.parent;
const parentKeys: string[] = [];
while (_curr) {
if (_curr.flags & ASTNodeFlags.VariableField) {
parentKeys.unshift(_curr.key);
}
_curr = _curr.parent;
}
return [...parentKeys, node.key];
}
/**
* 测试变量 Key Rename 的场景
*/
describe('test Listen Variable Key Rename', () => {
const container = getContainer();
const variableEngine = container.get(VariableEngine);
const renameService = container.get(VariableFieldKeyRenameService);
const testScope = variableEngine.createScope('test');
let renameInfo: { before: string[]; after: string[] } | null = null;
let disposeList: string[][] = [];
renameService.onRename((_rename) => {
renameInfo = {
before: getFieldKeys(_rename.before),
after: getFieldKeys(_rename.after),
};
});
renameService.onDisposeInList((_field) => {
disposeList.push(getFieldKeys(_field));
});
beforeEach(() => {
testScope.ast.set('test', simpleVariableList);
renameInfo = null;
disposeList = [];
});
const produceNextJSON = (producer: (json: any) => void) => {
const next = cloneDeep(simpleVariableList);
producer(next);
return next;
};
test.each([
// 更改一个字段
[
produceNextJSON((json) => {
json.declarations[0].key = 'string1111';
}),
['string'],
['string1111'],
[],
],
// 更改一个下钻字段
[
produceNextJSON((json) => {
json.declarations[4].type.properties[0].key = 'changedKey';
}),
['object', 'key1'],
['object', 'changedKey'],
[],
],
// 更改字段和他的类型
[
produceNextJSON((json) => {
json.declarations[0].key = 'string1111';
json.declarations[0].type = 'Number';
}),
null,
null,
[['string']],
],
// 更改多个下钻字段
[
produceNextJSON((json) => {
json.declarations[4].type.properties[0].key = 'changedKey';
json.declarations[4].type.properties[1].key = 'changedKey222';
}),
null,
null,
[
['object', 'key1'],
['object', 'key2'],
],
],
// 更改多个字段
[
produceNextJSON((json) => {
json.declarations[0].key = 'string1111';
json.declarations[1].key = 'boolean1111';
}),
null,
null,
[['string'], ['boolean']],
],
// 删除变量
[
produceNextJSON((json) => {
json.declarations = json.declarations.slice(1);
}),
null,
null,
[['string']],
],
// 添加变量
[
produceNextJSON((json) => {
json.declarations.push({ kind: 'String', key: 'newKey' });
}),
null,
null,
[],
],
])('test variable change', (_json, _before, _after, _disposeList) => {
testScope.ast.set('test', _json);
expect(renameInfo?.before || null).toEqual(_before);
expect(renameInfo?.after || null).toEqual(_after);
expect(disposeList).toEqual(_disposeList);
});
});
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { vi, describe, test, expect } from 'vitest';
import { VariableEngine } from '../../src';
import { simpleVariableList } from '../../__mocks__/variables';
import { getContainer } from '../../__mocks__/container';
vi.mock('nanoid', () => {
let mockId = 0;
return {
nanoid: () => 'mocked-id-' + mockId++,
};
});
/**
* 测试基本的变量声明场景
*/
describe('test Variable Engine', () => {
const container = getContainer();
const variableEngine = container.get(VariableEngine);
const globalVariableTable = variableEngine.globalVariableTable;
const testScope = variableEngine.createScope('test');
const simpleCase = testScope.ast.set('simple case', simpleVariableList);
test('test variable Engine Dispose', () => {
// unbind All will trigger @preDestroy
container.unbindAll();
expect(variableEngine.chain.disposed).toBeTruthy();
expect(testScope.disposed).toBeTruthy();
expect(simpleCase.disposed).toBeTruthy();
expect(globalVariableTable.variableKeys.length).toBe(0);
});
});