CI (SIAX Cloud) / integration (push) Has been skipped
masterplan-lock / check (push) Successful in 7s
masterplan-lock / check (pull_request) Successful in 5s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / security (push) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 13s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / quality (push) Successful in 36s
CI (SIAX Cloud) / quality (pull_request) Successful in 37s
CI (SIAX Cloud) / integration (pull_request) Successful in 43s
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
/**
|
|
* `migration-studio.test.ts` — HSP-085 acceptance:
|
|
* T01: an EJ importerbart objekt döljs inte ur nämnaren.
|
|
* T02: en uppgift kan återupptas efter sessionbyte.
|
|
* T03: en importerad CRM-lista marknadsförs inte som komplett M365-ersättning.
|
|
* Runner: vitest (0pt-konventionen).
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
computeImportRatio,
|
|
resumeMigration,
|
|
buildCoverageClaim,
|
|
type MigrationSourceObject,
|
|
type MigrationSession,
|
|
} from '../../src/connectors/migration-studio';
|
|
|
|
function sourceSet(): MigrationSourceObject[] {
|
|
return [
|
|
{ objectId: 'obj_1', name: 'CRM-lista: kunder', importable: true, tenantId: 't_1' },
|
|
{ objectId: 'obj_2', name: 'CRM-lista: leads', importable: true, tenantId: 't_1' },
|
|
{ objectId: 'obj_3', name: 'M365 mail-box', importable: false, nonImportableReason: 'mail är utanför migrationsscopet (p0st äger mail)', tenantId: 't_1' },
|
|
{ objectId: 'obj_4', name: 'Cross-tenant doc', importable: false, nonImportableReason: 'annan tenant', tenantId: 't_other' },
|
|
];
|
|
}
|
|
|
|
describe('HSP-085-T01: ej importerbart objekt döljs inte ur nämnaren', () => {
|
|
it('the ratio counts EVERY source object in the denominator (non-importable stays)', () => {
|
|
const ratio = computeImportRatio(sourceSet());
|
|
expect(ratio.totalSourceObjects).toBe(4);
|
|
expect(ratio.importableCount).toBe(2);
|
|
expect(ratio.nonImportableCount).toBe(2);
|
|
// 2/4 = 0.5 — NOT 2/2 = 1.0 (which would be hiding the failures).
|
|
expect(ratio.ratio).toBe(0.5);
|
|
});
|
|
|
|
it('the non-importable objects are ALWAYS listed with their reasons', () => {
|
|
const ratio = computeImportRatio(sourceSet());
|
|
expect(ratio.nonImportable).toHaveLength(2);
|
|
expect(ratio.nonImportable.some((n) => n.reason.includes('mail'))).toBe(true);
|
|
expect(ratio.nonImportable.some((n) => n.reason.includes('annan tenant'))).toBe(true);
|
|
});
|
|
|
|
it('a fully-importable source gives ratio 1.0 (honest, no non-importable to hide)', () => {
|
|
const ratio = computeImportRatio([
|
|
{ objectId: 'a', name: 'a', importable: true, tenantId: 't' },
|
|
]);
|
|
expect(ratio.ratio).toBe(1);
|
|
expect(ratio.nonImportable).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('HSP-085-T02: uppgift kan återupptas efter sessionbyte', () => {
|
|
const stored: MigrationSession = {
|
|
migrationId: 'mig_1',
|
|
tenantId: 't_1',
|
|
checkpointCursor: 'batch_3',
|
|
resolvedConflicts: ['conflict_a', 'conflict_b'],
|
|
state: 'conflict_review',
|
|
};
|
|
|
|
it('a session change RESUMES from the stored checkpoint + resolved conflicts', () => {
|
|
const result = resumeMigration(stored, 't_1');
|
|
expect(result.state).toBe('resumed');
|
|
expect(result.checkpointCursor).toBe('batch_3');
|
|
expect(result.resolvedConflicts).toEqual(['conflict_a', 'conflict_b']);
|
|
});
|
|
|
|
it('a session change to a DIFFERENT tenant starts FRESH (no cross-tenant resume)', () => {
|
|
const result = resumeMigration(stored, 't_other');
|
|
expect(result.state).toBe('fresh');
|
|
expect(result.checkpointCursor).toBe('0');
|
|
});
|
|
|
|
it('no stored session starts fresh', () => {
|
|
const result = resumeMigration(undefined, 't_1');
|
|
expect(result.state).toBe('fresh');
|
|
expect(result.checkpointCursor).toBe('0');
|
|
});
|
|
});
|
|
|
|
describe('HSP-085-T03: CRM-lista marknadsförs inte som komplett M365-ersättning', () => {
|
|
it('the coverage claim is SCOPED to what was imported (never a generic total)', () => {
|
|
const ratio = computeImportRatio(sourceSet());
|
|
const claim = buildCoverageClaim('CRM-listor', ratio);
|
|
expect(claim.scope).toContain('CRM-listor');
|
|
expect(claim.scope).toContain('2 av 4');
|
|
expect(claim.scope).toContain('ej importerbarta');
|
|
// NEVER claims a generic total badge.
|
|
expect(claim.scope.toLowerCase()).not.toContain('complete');
|
|
expect(claim.scope.toLowerCase()).not.toContain('komplett microsoft');
|
|
});
|
|
|
|
it('the claim carries the NON-IMPORTABLE count (the honest denominator)', () => {
|
|
const ratio = computeImportRatio(sourceSet());
|
|
const claim = buildCoverageClaim('CRM-listor', ratio);
|
|
expect(claim.nonImportableCount).toBe(2);
|
|
expect(claim.totalSourceObjects).toBe(4);
|
|
});
|
|
}); |