/* * What an area id is, and whose area it is. * * The item panel shows a location's parent. When the parent is an area, the host answers what that id is * (`external: 'space.area'`). Nothing registered that answer until 2026-09-18, so every location's parent read * "no component can resolve this relation" on the running twin. * * The lookup is (domain, space, areaId) — the area table's own unique key. An `areaId` is unique inside a space, * so the same id can exist in two spaces of one tenant; the space comes from the twin the relation belongs to. * Held here: the name of this twin's area, "missing" for an id that is in no area of this space, and another * space's area of the same id never answering. */ import { test, before, after } from 'node:test' import assert from 'node:assert/strict' import { createRequire } from 'node:module' import type { DataSource } from 'typeorm' const req = createRequire(import.meta.url) const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell') const { entities: authEntities } = req('@things-factory/auth-base') const { entities: twinEntities } = req('../dist-server/service/index.js') const { resolveAreas, installAreaResolver, SPACE_AREA_ENTITY } = req('../dist-server/service/twin-space/area-resolver.js') const { externalResolverEntities, clearExternalResolvers, resolveExternal } = req('../dist-server/service/twin-model/external-resolver.js') const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js') const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js') const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js') const { DataSource: TypeOrmDataSource } = req('typeorm') let ds: DataSource let domainId: string let otherDomainId: string before(async () => { ds = new TypeOrmDataSource({ type: 'sqlite', database: ':memory:', synchronize: true, dropSchema: true, namingStrategy: new NamingStrategy(), entities: [...shellEntities, ...authEntities, ...twinEntities], logging: false }) await ds.initialize() addDataSource('default', ds) const d = await ds.getRepository(Domain).save({ name: 'area-test', subdomain: 'area-test' } as any) const other = await ds.getRepository(Domain).save({ name: 'area-other', subdomain: 'area-other' } as any) domainId = d.id otherDomainId = other.id const spaces = ds.getRepository(TwinSpace) const north = await spaces.save({ domain: { id: domainId }, spaceId: 'north', name: 'North site' } as any) const south = await spaces.save({ domain: { id: domainId }, spaceId: 'south', name: 'South site' } as any) const far = await spaces.save({ domain: { id: otherDomainId }, spaceId: 'far', name: 'Far site' } as any) const areas = ds.getRepository(TwinArea) /* The same areaId in two spaces of one tenant — that is what the space scope is for. */ await areas.save({ domain: { id: domainId }, space: { id: north.id }, areaId: 'hall-a', name: 'North Hall A' } as any) await areas.save({ domain: { id: domainId }, space: { id: south.id }, areaId: 'hall-a', name: 'South Hall A' } as any) await areas.save({ domain: { id: otherDomainId }, space: { id: far.id }, areaId: 'hall-a', name: 'Far Hall A' } as any) /* An area with no name — the answer then carries the id alone, and does not invent one. */ await areas.save({ domain: { id: domainId }, space: { id: north.id }, areaId: 'annex' } as any) const instances = ds.getRepository(TwinInstance) await instances.save({ domain: { id: domainId }, instanceId: 'north-twin', kind: 'wms', restartPolicy: 'resume', purpose: 'operational', spaceId: 'north', status: 'stopped' } as any) await instances.save({ domain: { id: domainId }, instanceId: 'south-twin', kind: 'wms', restartPolicy: 'resume', purpose: 'operational', spaceId: 'south', status: 'stopped' } as any) await instances.save({ domain: { id: domainId }, instanceId: 'homeless-twin', kind: 'wms', restartPolicy: 'resume', purpose: 'operational', status: 'stopped' } as any) }) after(async () => { clearExternalResolvers() await ds?.destroy() }) test('an area of this twin comes back with its name', async () => { const refs = await resolveAreas(domainId, ['hall-a'], { instanceId: 'north-twin' }) assert.deepEqual(refs, [{ id: 'hall-a', name: 'North Hall A' }]) }) test('the same areaId in another space of the same tenant never answers', async () => { const south = await resolveAreas(domainId, ['hall-a'], { instanceId: 'south-twin' }) assert.deepEqual(south, [{ id: 'hall-a', name: 'South Hall A' }], 'each twin sees its own space') const other = await resolveAreas(otherDomainId, ['hall-a'], { instanceId: 'north-twin' }) assert.deepEqual(other, [{ id: 'hall-a', missing: true }], 'a twin of another tenant is not this tenant’s twin') }) test('an id that is in no area of this space is missing — it is not dropped', async () => { const refs = await resolveAreas(domainId, ['hall-a', 'nowhere'], { instanceId: 'north-twin' }) assert.deepEqual(refs, [{ id: 'hall-a', name: 'North Hall A' }, { id: 'nowhere', missing: true }]) }) test('an area with no name answers with the id alone — no name is invented', async () => { const refs = await resolveAreas(domainId, ['annex'], { instanceId: 'north-twin' }) assert.deepEqual(refs, [{ id: 'annex' }]) }) test('a twin with no space cannot have area parents, and says so', async () => { const warned: string[] = [] const refs = await resolveAreas(domainId, ['hall-a'], { instanceId: 'homeless-twin' }, m => warned.push(m)) assert.deepEqual(refs, [{ id: 'hall-a', missing: true }]) assert.equal(warned.length, 1, 'the reason is said once, not swallowed') }) test('the resolver is registered under the word the contract uses, and answers through the seam', async () => { clearExternalResolvers() installAreaResolver() assert.ok(externalResolverEntities().includes(SPACE_AREA_ENTITY)) assert.equal(SPACE_AREA_ENTITY, 'space.area') const got = await resolveExternal(SPACE_AREA_ENTITY, domainId, ['hall-a'], { instanceId: 'north-twin' }) assert.deepEqual(got, { refs: [{ id: 'hall-a', name: 'North Hall A' }] }) })