import test from 'node:test'; import assert from 'node:assert/strict'; import { selectAffiliationSummary, selectMajorOrganizationTotals, selectOrganizationPositionCounts, selectStatusGroups } from './content-selectors.js'; import { calculateSummary } from './summary.js'; import type { OrgChartDataset } from './types.js'; function fixture(): OrgChartDataset { return { schemaVersion: '0.1-draft', organizations: [ { id: 'root', parentId: null, name: '가상 본부', type: 'root', leaderId: 'leader', displayOrder: 0 }, { id: 'team', parentId: 'root', name: '가상 팀', type: 'team', leaderId: null, displayOrder: 1 }, { id: 'empty', parentId: 'root', name: '빈 가상 팀', type: 'team', leaderId: null, displayOrder: 2 }, ], employees: [ { id: 'leader', organizationId: 'root', affiliationId: 'first', name: '가상 가', position: '대표', jobTitle: '본부장', profileImage: null, workStatus: 'active' }, { id: 'sent', organizationId: 'team', affiliationId: 'second', name: '가상 나', position: '책임', jobTitle: null, profileImage: null, workStatus: 'dispatched' }, { id: 'leave', organizationId: 'team', affiliationId: 'first', name: '가상 다', position: '책임', jobTitle: null, profileImage: null, workStatus: 'onLeave' }, ], affiliations: [{ id: 'first', name: '가상 관계사 가' }, { id: 'second', name: '가상 관계사 나' }, { id: 'zero', name: '가상 관계사 다' }], metadata: { asOfDate: '2026-10-12', dataVersion: 'selector-test' }, }; } test('position matrix uses direct primary membership, includes leaders and all work statuses', () => { const data = fixture(); const matrix = selectOrganizationPositionCounts(data); const summary = calculateSummary(data); assert.deepEqual([...matrix.get('root')!], [['대표', 1]]); assert.deepEqual([...matrix.get('team')!], [['책임', 2]]); assert.equal(matrix.get('empty')!.size, 0); assert.equal(matrix.get('root')!.get('책임'), undefined); assert.equal(summary.raw.byOrganization.root!.inclusive, 3); for (const organization of data.organizations) { assert.equal([...matrix.get(organization.id)!.values()].reduce((sum, count) => sum + count, 0), summary.raw.byOrganization[organization.id]!.direct); } for (const [position, count] of Object.entries(summary.raw.byPosition)) { assert.equal([...matrix.values()].reduce((sum, row) => sum + (row.get(position) ?? 0), 0), count); } assert.equal([...matrix.values()].flatMap(row => [...row.values()]).reduce((sum, count) => sum + count, 0), summary.raw.total); assert.equal(summary.aggregation.status, 'unavailable'); }); test('duplicate IDs cannot inflate matrix or status groups and original Employee references survive', () => { const data = fixture(); data.employees.push({ ...data.employees[1]!, organizationId: 'root', position: '대표' }); const summary = calculateSummary(data); const matrix = selectOrganizationPositionCounts(data); assert.equal(matrix.get('team')!.get('책임'), 2); assert.equal(matrix.get('root')!.get('대표'), 1); const groups = selectStatusGroups(data, summary); assert.equal(groups[0]!.employees.length, 1); assert.strictEqual(groups[0]!.employees[0], data.employees[1]); assert.strictEqual(groups[1]!.employees[0], data.employees[2]); assert.equal(summary.raw.total, 3); }); test('opaque prototype-like organization IDs and position strings remain ordinary matrix keys', () => { const data = fixture(); data.organizations[1]!.id = '__proto__'; data.employees[1]!.organizationId = '__proto__'; data.employees[2]!.organizationId = '__proto__'; data.employees[1]!.position = 'constructor'; data.employees[2]!.position = '__proto__'; const row = selectOrganizationPositionCounts(data).get('__proto__')!; assert.equal(row.get('constructor'), 1); assert.equal(row.get('__proto__'), 1); }); test('affiliation summary preserves declared order, zero entries and original affiliation objects', () => { const data = fixture(); const summary = calculateSummary(data); const rows = selectAffiliationSummary(data, summary); assert.deepEqual(rows.map(row => [row.affiliation.id, row.count]), [['first', 2], ['second', 1], ['zero', 0]]); assert.strictEqual(rows[2]!.affiliation, data.affiliations[2]); assert.equal(rows.reduce((sum, row) => sum + row.count, 0), summary.raw.total); }); test('status flags independently hide groups without changing Raw Count or affiliation totals', () => { const data = fixture(); const summary = calculateSummary(data); data.presentation = { title: '가상 조직도', description: null, hubText: null, organizationOrder: [], groups: { showEmployees: false, showSummary: false, showAffiliations: false, showDispatched: false, showOnLeave: true }, }; assert.deepEqual(selectStatusGroups(data, summary).map(group => group.status), ['onLeave']); data.presentation.groups.showDispatched = true; data.presentation.groups.showOnLeave = false; assert.deepEqual(selectStatusGroups(data, summary).map(group => group.status), ['dispatched']); data.presentation.groups.showDispatched = false; assert.deepEqual(selectStatusGroups(data, summary), []); assert.deepEqual(calculateSummary(data), summary); assert.equal(selectAffiliationSummary(data, summary).length, 3); }); test('zero employees retains all organization/affiliation rows and both empty status groups', () => { const data = fixture(); data.employees = []; data.organizations[0]!.leaderId = null; const summary = calculateSummary(data); assert.equal(selectOrganizationPositionCounts(data).size, 3); assert.ok([...selectOrganizationPositionCounts(data).values()].every(row => row.size === 0)); assert.ok(selectAffiliationSummary(data, summary).every(row => row.count === 0)); assert.deepEqual(selectStatusGroups(data, summary).map(group => [group.status, group.count, group.employees.length]), [['dispatched', 0, 0], ['onLeave', 0, 0]]); }); test('new JSON snapshot updates matrix, affiliations and status groups without a cached count', () => { const data = fixture(); const before = calculateSummary(data); data.employees[1]!.organizationId = 'root'; data.employees[1]!.position = '수석'; data.employees[1]!.affiliationId = 'zero'; data.employees[1]!.workStatus = 'active'; const after = calculateSummary(data); const matrix = selectOrganizationPositionCounts(data); assert.equal(matrix.get('root')!.get('수석'), 1); assert.equal(matrix.get('team')!.get('책임'), 1); assert.deepEqual(selectAffiliationSummary(data, after).map(row => row.count), [2, 0, 1]); assert.equal(selectStatusGroups(data, after)[0]!.employees.length, 0); assert.equal(before.raw.byStatus.dispatched, 1); assert.equal(after.raw.total, before.raw.total); }); test('major organization totals: root direct members then inclusive root children, summing to the raw total', () => { const data = fixture(); data.organizations.push({ id: 'cell', parentId: 'team', name: '가상 셀', type: 'cell', leaderId: null, displayOrder: 3 }); data.employees.push({ id: 'member', organizationId: 'cell', affiliationId: 'zero', name: '가상 라', position: '선임', jobTitle: null, profileImage: null, workStatus: 'active' }); const summary = calculateSummary(data); const rows = selectMajorOrganizationTotals(data, summary); assert.deepEqual(rows.map(row => [row.organization.id, row.scope, row.count]), [['root', 'direct', 1], ['team', 'inclusive', 3], ['empty', 'inclusive', 0]]); assert.equal(rows.reduce((sum, row) => sum + row.count, 0), summary.raw.total); assert.equal(rows.some(row => row.organization.id === 'cell'), false); });