XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
import { describe, expect, it } from 'vitest';
import type { TriggerExpression, TriggerProgramV2, TriggerStatement } from '../types';
import { insertTriggerNode, parseTriggerNode, serializeTriggerNode, triggerProgramFitsLimits } from './trigger-program-clipboard';

const literal = (value: unknown): TriggerExpression => ({ nodeId: crypto.randomUUID(), kind: 'LITERAL',
  name: '', valueType: 'any', literal: value, arguments: [] });
const statement = (overrides: Partial<TriggerStatement> = {}): TriggerStatement => ({
  nodeId: crypto.randomUUID(), kind: 'ACTION', name: 'send_player', inputs: { message: literal('Hello') },
  statements: [], elseStatements: [], cases: [], ...overrides,
});
const program = (statements: TriggerStatement[] = []): TriggerProgramV2 => ({ schemaVersion: 2,
  events: [{ nodeId: 'event', type: 'player.join', configuration: { nodeId: 'configuration-data' } }],
  declarations: [], functions: [], statements,
});

describe('V2 program node clipboard', () => {
  it('round-trips each supported node category and rejects malformed or oversized clipboard input', () => {
    const value = program();
    value.declarations.push({ nodeId: 'variable', name: 'progress', valueType: 'integer', storage: 'player',
      visibility: 'trigger', lifetime: 'PERSISTENT', initialValue: literal(1) });
    value.functions.push({ nodeId: 'function', name: 'reward', parameters: [], returnType: 'void', locals: [], statements: [] });
    for (const node of [
      { category: 'event' as const, value: value.events[0] },
      { category: 'declaration' as const, value: value.declarations[0] },
      { category: 'function' as const, value: value.functions[0] },
      { category: 'statement' as const, value: statement() },
    ]) expect(parseTriggerNode(serializeTriggerNode(node))).toEqual(node);
    expect(parseTriggerNode('ordinary text')).toBeUndefined();
    expect(parseTriggerNode('x'.repeat(1_048_577))).toBeUndefined();
    expect(parseTriggerNode('{"marker":"xfesm-trigger-node-v2","category":"statement","value":{}}')).toBeUndefined();
    expect(parseTriggerNode(serializeTriggerNode({ category: 'event', value: statement() }))).toBeUndefined();
    const broken = statement({ expression: { ...literal(1), arguments: [null as unknown as TriggerExpression] } });
    expect(parseTriggerNode(serializeTriggerNode({ category: 'statement', value: broken }))).toBeUndefined();
  });

  it('copies a complete subtree with fresh AST IDs without altering literal records or its source', () => {
    const child = statement({ inputs: { record: literal({ nodeId: 'user-data', arguments: [{ nodeId: 'also-data' }] }) } });
    const root = statement({ kind: 'IF', name: '', expression: literal(true), statements: [child],
      cases: [{ nodeId: 'case', match: literal(2), statements: [statement()] }], elseStatements: [statement()] });
    const before = structuredClone(root);
    const source = program([root]);
    const result = insertTriggerNode(source, { category: 'statement', value: root }, root.nodeId);
    const copy = result.program.statements[1];
    expect(root).toEqual(before);
    expect(result.program.statements[0]).toEqual(before);
    expect(copy.nodeId).not.toBe(root.nodeId);
    expect(copy.expression?.nodeId).not.toBe(root.expression?.nodeId);
    expect(copy.statements[0].nodeId).not.toBe(child.nodeId);
    expect(copy.statements[0].inputs.record.nodeId).not.toBe(child.inputs.record.nodeId);
    expect(copy.statements[0].inputs.record.literal).toEqual(child.inputs.record.literal);
    expect(copy.cases[0].nodeId).not.toBe(root.cases[0].nodeId);
    expect(copy.cases[0].match.nodeId).not.toBe(root.cases[0].match.nodeId);
    expect(copy.elseStatements[0].nodeId).not.toBe(root.elseStatements[0].nodeId);
  });

  it('inserts after a nested statement in its own branch instead of moving it to the program root', () => {
    const child = statement();
    const root = statement({ kind: 'IF', elseStatements: [child] });
    const result = insertTriggerNode(program([root]), { category: 'statement', value: child }, child.nodeId);
    expect(result.program.statements).toHaveLength(1);
    expect(result.program.statements[0].elseStatements).toHaveLength(2);
    expect(result.program.statements[0].elseStatements[1].nodeId).toBe(result.nodeId);
  });

  it('targets empty then/else and switch branches explicitly', () => {
    const root = statement({ kind: 'SWITCH', cases: [{ nodeId: 'case', match: literal('boss'), statements: [] }] });
    for (const branch of ['statements', 'elseStatements', 'cases'] as const) {
      const result = insertTriggerNode(program([root]), { category: 'statement', value: statement() }, root.nodeId,
        { parentId: root.nodeId, branch, caseId: branch === 'cases' ? 'case' : undefined });
      expect(result.program.statements).toHaveLength(1);
      const nested = result.program.statements[0];
      expect(branch === 'cases' ? nested.cases[0].statements : nested[branch]).toHaveLength(1);
    }
  });

  it('pastes into a selected function and retains sibling positions within function bodies', () => {
    const child = statement();
    const value = program();
    value.functions.push({ nodeId: 'function', name: 'reward', parameters: [], returnType: 'void', locals: [], statements: [child] });
    for (const selected of ['function', child.nodeId]) {
      const result = insertTriggerNode(value, { category: 'statement', value: child }, selected);
      expect(result.program.statements).toHaveLength(0);
      expect(result.program.functions[0].statements).toHaveLength(2);
    }
  });

  it('avoids declaration/function name collisions and keeps recursive calls within copied functions', () => {
    const value = program();
    value.declarations.push({ nodeId: 'variable', name: 'progress', valueType: 'integer', storage: 'player',
      visibility: 'trigger', lifetime: 'PERSISTENT', initialValue: literal(1) });
    const copied = insertTriggerNode(value, { category: 'declaration', value: value.declarations[0] }, 'variable');
    expect(copied.program.declarations[1].name).toBe('progress_copy');
    const twice = insertTriggerNode(copied.program, { category: 'declaration', value: value.declarations[0] }, 'variable');
    expect(twice.program.declarations[1].name).toBe('progress_copy2');
    value.functions.push({ nodeId: 'function', name: 'reward', parameters: [], returnType: 'void', locals: [],
      statements: [statement({ kind: 'CALL', name: 'reward' })] });
    const fn = insertTriggerNode(value, { category: 'function', value: value.functions[0] }, 'function');
    expect(fn.program.functions[1].name).toBe('reward_copy');
    expect(fn.program.functions[1].statements[0].name).toBe('reward_copy');
    expect(fn.program.functions[0].statements[0].name).toBe('reward');
  });

  it('preserves event configuration verbatim and enforces resulting event/node/depth limits', () => {
    const value = program([statement()]);
    const result = insertTriggerNode(value, { category: 'event', value: value.events[0] }, 'event');
    expect(result.program.events[1].configuration.nodeId).toBe('configuration-data');
    expect(result.program.events[1].nodeId).not.toBe('event');
    expect(triggerProgramFitsLimits(result.program)).toBe(true);
    expect(triggerProgramFitsLimits(result.program, 1)).toBe(false);
    expect(triggerProgramFitsLimits({ ...value, events: Array.from({ length: 33 }, () => value.events[0]) })).toBe(false);
    let nested = statement();
    for (let index = 0; index < 34; index++) nested = statement({ kind: 'IF', statements: [nested] });
    expect(parseTriggerNode(serializeTriggerNode({ category: 'statement', value: nested }))).toBeUndefined();
    expect(triggerProgramFitsLimits(program([nested]))).toBe(false);
  });
});