File size: 6,436 Bytes
970fc3e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import test from 'node:test';
import assert from 'node:assert/strict';
import {readdir, readFile} from 'node:fs/promises';
import {fileURLToPath} from 'node:url';
import {join, relative} from 'node:path';
import zh from './fixtures/zh.json' with {type: 'json'};
import {parseTransformText} from '../src/shared/transforms.mjs';
import {parseDirectorCommand} from '../src/shared/actors.mjs';
import {cameraViewIntent} from '../src/shared/camera-view-intent.mjs';
import {productionSpecialist} from '../src/shared/production-intent.mjs';
import {entityLabel} from '../src/shared/entity-label.mjs';
import {uiText} from '../src/shared/ui-text.mjs';
import {languagePattern, localizedReply} from '../src/shared/language.mjs';
import {compileCameraCommand} from '../src/agents/rig-runner.mjs';
import {AGENTS, agentPrompt} from '../src/agents/registry.mjs';
import {scanAnnotationPrompt} from '../src/shared/scan-furnishing.mjs';
import {scanStructure} from '../src/shared/scan-structure.mjs';
import {reconstructScan} from '../src/shared/scan-reconstruction.mjs';

const root = fileURLToPath(new URL('../', import.meta.url));
const localizedCharacters = /[\p{Script=Han}\u3000-\u303f\uff01-\uff65]/u;

async function filesUnder(directory) {
  const files = [];
  for (const entry of await readdir(directory, {withFileTypes: true})) {
    const path = join(directory, entry.name);
    if (entry.isDirectory()) files.push(...await filesUnder(path));
    else if (/\.(mjs|json|html|css|md)$/.test(entry.name)) files.push(path);
  }
  return files;
}

test('public source is English with multilingual data serialized separately', async () => {
  const files = [join(root, 'README.md'), join(root, 'server.mjs')];
  for (const folder of ['src', 'public', 'scripts', 'tests']) files.push(...await filesUnder(join(root, folder)));
  for (const path of files) {
    assert(!localizedCharacters.test(relative(root, path)), `Localized filename: ${path}`);
    assert(!localizedCharacters.test(await readFile(path, 'utf8')), `Localized source text: ${path}`);
  }
});

test('English and Chinese transforms retain distances, angles and Chinese numerals', () => {
  for (const {input, english, type, offset, degrees} of zh.transformCases) {
    const plan = parseTransformText(input);
    assert.deepEqual(plan, parseTransformText(english));
    assert.equal(plan.type, type);
    if (offset) assert.deepEqual(plan.offset, offset);
    if (degrees !== undefined) assert.equal(plan.degrees, degrees);
  }
  assert.deepEqual(parseDirectorCommand(zh.delayRequest), {type: 'delay', delay: 5});
  assert.deepEqual(parseDirectorCommand(zh.motionNumberRequest), {type: 'motion', number: 3});
});

test('bilingual specialist routing and camera commands survive language extraction', () => {
  const scene = {objects: [{id: 'box', kind: 'box'}], floods: []};
  for (const {input, english, route} of zh.specialistCases) {
    const options = {scene, ids: ['box']};
    assert.equal(productionSpecialist(input, options), route);
    assert.equal(productionSpecialist(english, options), route);
  }
  assert.equal(cameraViewIntent('call camera agent'), 'preview');
  for (const {input, action} of zh.cameraCases) assert.equal(cameraViewIntent(input), action);
});

test('legacy saved labels translate for display without rewriting user data', () => {
  const entity = {id: 'floor-4', name: zh.legacyFloorLabel, shape: 'box'};
  const original = structuredClone(entity);
  assert.match(entityLabel(entity), /^Floor (section|area) 4$/);
  assert.deepEqual(entity, original);
  assert.equal(uiText(zh.legacyGroundHint), 'Hold the right trigger to draw on the floor. Release to finish.');
  assert.equal(uiText('Camera 2'), 'Camera 2');
  assert(languagePattern('scene.scanTitleSuffix').test(zh.legacyScanTitle));
  assert(languagePattern('scene.scanTitleSuffix').test('Room · Scan structure preview'));
});

test('language rules do not share mutable regular-expression state', () => {
  const first = languagePattern('transform.quantity');
  first.exec('2 meters');
  assert(first.lastIndex > 0);
  const second = languagePattern('transform.quantity');
  assert.equal(second.lastIndex, 0);
  assert(second.test('2 meters'));
  assert.throws(() => languagePattern('missing.rule'), /Unknown language pattern/);
});

test('English role prompts preserve original user language and localized Agent replies', () => {
  for (const [id, role] of Object.entries(AGENTS)) {
    assert(!localizedCharacters.test(role.name + role.description + role.instructions));
    assert(agentPrompt(id, {request: zh.waveRequest}).includes(zh.waveRequest));
  }
  const prompt = scanAnnotationPrompt({objects: []}, [], 4, 'Label the measured furniture');
  assert(prompt.includes('Use short English names'));
  assert(!prompt.includes('Chinese names'));
  const result = compileCameraCommand({action: 'create', reply: zh.previewReply}, {mode: 'explore', scene: {objects: []}});
  assert.equal(result.action, 'none');
  assert.equal(result.reply, localizedReply('editRequired', 'zh'));
  assert(/\p{Script=Han}/u.test(result.reply));
  assert.equal(localizedReply('closureRepaired', 'unknown'), localizedReply('closureRepaired', 'en'));
});

test('scan reconstruction retains window roles and table legs after renaming in English', () => {
  const horizontal = (label, y, x = 2, z = 2) => ({label, orientation: 'horizontal', points: [[-x,y,-z],[x,y,-z],[x,y,z],[-x,y,z]]});
  const wall = (a, b) => ({label: 'wall', orientation: 'vertical', points: [[a[0],0,a[1]],[b[0],0,b[1]],[b[0],3,b[1]],[a[0],3,a[1]]]});
  const planes = [horizontal('floor', 0), horizontal('ceiling', 3),
    wall([-2,-2], [2,-2]), wall([2,-2], [2,2]), wall([2,2], [-2,2]), wall([-2,2], [-2,-2]),
    horizontal('table', 0.8, 0.6, 0.4),
    {label: 'window', orientation: 'vertical', points: [[-0.5,1,-2],[0.5,1,-2],[0.5,2,-2],[-0.5,2,-2]]}];
  const scanned = scanStructure(planes);
  const blueprint = {planes, alignment: scanned.alignment, referenceFloor: scanned.scene.objects.find(o => o.id === 'ground')};
  const rebuilt = reconstructScan(blueprint);
  const window = rebuilt.objects.find(o => o.scanAnchorId === 'surface-7');
  assert.equal(window.role, 'window');
  const legs = rebuilt.objects.filter(o => /-support-(left|right)$/.test(o.id));
  assert.equal(legs.length, 2);
  assert(legs.every(o => o.name.endsWith('table leg')));
  assert(rebuilt.objects.every(o => !localizedCharacters.test(o.name)));
});