0
0
Fork 0
Codesys-MCP-SP21-plus/tests/unit/version-pin.test.ts
steel 320646b8fe
Some checks failed
CI / build-and-test (18.x) (push) Has been cancelled
CI / build-and-test (20.x) (push) Has been cancelled
CI / build-and-test (22.x) (push) Has been cancelled
fixup
2026-08-19 18:39:47 +08:00

195 lines
7 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
parseProfileVersion,
parseProfileLabel,
parsePinText,
parseLibraryMdProfile,
resolveVersionPin,
decideVersionPin,
label,
} from '../../src/version-pin';
describe('parseProfileVersion', () => {
it('maps rawPatch/10 to the install-dir patch convention', () => {
expect(parseProfileVersion('3.5.19.20')).toEqual({ sp: 19, patch: 2 });
expect(parseProfileVersion('3.5.21.50')).toEqual({ sp: 21, patch: 5 });
expect(parseProfileVersion('3.5.22.10')).toEqual({ sp: 22, patch: 1 });
});
it('treats a zero raw patch as patch 0', () => {
expect(parseProfileVersion('3.5.18.0')).toEqual({ sp: 18, patch: 0 });
});
it('returns undefined for non-version text', () => {
expect(parseProfileVersion('MSP-Con V3.5 SP19')).toBeUndefined();
});
});
describe('parseProfileLabel', () => {
it('parses SP with and without a patch', () => {
expect(parseProfileLabel('MSP-Con V3.5 SP21 Patch 5')).toEqual({ sp: 21, patch: 5 });
expect(parseProfileLabel('MSP-Con V3.5 SP19')).toEqual({ sp: 19, patch: 0 });
});
it('returns undefined when there is no SP token', () => {
expect(parseProfileLabel('3.5.19.20')).toBeUndefined();
});
});
describe('parsePinText', () => {
it('accepts either form', () => {
expect(parsePinText('3.5.19.20')).toEqual({ sp: 19, patch: 2 });
expect(parsePinText('MSP-Con V3.5 SP19')).toEqual({ sp: 19, patch: 0 });
});
it('prefers the dotted version when a line carries both', () => {
expect(parsePinText('MSP-Con V3.5 SP21 Patch 5 (3.5.21.50)')).toEqual({ sp: 21, patch: 5 });
});
});
describe('parseLibraryMdProfile', () => {
const row =
'| MSP-Con Development System | `MSP-Con.exe MSP-Con V3.5 SP21 Patch 5, ScriptEngine.plugin 4.2.0.0` |';
it('reads the SP/patch out of the generated table row', () => {
expect(parseLibraryMdProfile(`# Library inventory\n\n${row}\n`)).toEqual({ sp: 21, patch: 5 });
});
it('does not mistake the ScriptEngine version for a profile version', () => {
// Regression: a naive parseProfileVersion on this row matches "4.2.0.0".
expect(parseLibraryMdProfile(row)).toEqual({ sp: 21, patch: 5 });
});
it('returns undefined when the row is absent', () => {
expect(parseLibraryMdProfile('| Project Information.Version | `1.0.0.0` |')).toBeUndefined();
});
});
describe('resolveVersionPin', () => {
let dir: string;
let proj: string;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'version-pin-'));
proj = path.join(dir, 'Thing.project');
fs.writeFileSync(proj, 'not-a-zip');
});
afterEach(() => {
fs.rmSync(dir, { recursive: true, force: true });
});
it('returns undefined when neither source exists', () => {
expect(resolveVersionPin(proj)).toBeUndefined();
});
it('reads library.md when no pin file is present', () => {
fs.writeFileSync(
path.join(dir, 'library.md'),
'| MSP-Con Development System | `MSP-Con.exe MSP-Con V3.5 SP22 Patch 1` |'
);
expect(resolveVersionPin(proj)).toMatchObject({ sp: 22, patch: 1, source: 'library.md' });
});
it('prefers .codesys-version over library.md', () => {
fs.writeFileSync(
path.join(dir, 'library.md'),
'| MSP-Con Development System | `MSP-Con.exe MSP-Con V3.5 SP22 Patch 1` |'
);
fs.writeFileSync(path.join(dir, '.codesys-version'), '3.5.19.20\n');
expect(resolveVersionPin(proj)).toMatchObject({ sp: 19, patch: 2, source: '.codesys-version' });
});
it('skips comments and blank lines in the pin file', () => {
fs.writeFileSync(
path.join(dir, '.codesys-version'),
'# authored on the MarinerX house standard\n\n3.5.19.20\n'
);
expect(resolveVersionPin(proj)).toMatchObject({ sp: 19, patch: 2 });
});
it('falls through to library.md when the pin file is unparseable', () => {
fs.writeFileSync(path.join(dir, '.codesys-version'), 'whatever\n');
fs.writeFileSync(
path.join(dir, 'library.md'),
'| MSP-Con Development System | `MSP-Con.exe MSP-Con V3.5 SP22 Patch 1` |'
);
expect(resolveVersionPin(proj)).toMatchObject({ sp: 22, patch: 1, source: 'library.md' });
});
});
describe('decideVersionPin', () => {
const projectFilePath = 'C:\\plc\\Thing\\Thing.project';
const pin = (sp: number, patch: number) =>
({ sp, patch, source: '.codesys-version' as const, raw: `3.5.${sp}.${patch * 10}` });
it('proceeds on an exact match', () => {
expect(
decideVersionPin(pin(19, 2), { sp: 19, patch: 2 }, { saves: true, projectFilePath })
).toEqual({ action: 'proceed' });
});
it('refuses a saving tool when the server is newer', () => {
const d = decideVersionPin(pin(19, 2), { sp: 21, patch: 5 }, { saves: true, projectFilePath });
expect(d.action).toBe('refuse');
expect(d.action === 'refuse' && d.message).toContain('UPGRADE');
});
it('refuses a saving tool when the server is older', () => {
const d = decideVersionPin(pin(22, 1), { sp: 21, patch: 5 }, { saves: true, projectFilePath });
expect(d.action).toBe('refuse');
expect(d.action === 'refuse' && d.message).toContain('DOWNGRADE');
});
it('treats a patch-level difference as a mismatch too', () => {
// The MarinerX7 failure was an SP difference, but SP21 P5 -> SP21 P2 also
// rewrites the file; nothing here should special-case same-SP.
const d = decideVersionPin(pin(21, 2), { sp: 21, patch: 5 }, { saves: true, projectFilePath });
expect(d.action).toBe('refuse');
});
it('only warns a read-only tool on mismatch', () => {
const d = decideVersionPin(pin(19, 2), { sp: 21, patch: 5 }, { saves: false, projectFilePath });
expect(d.action).toBe('proceed-with-warning');
expect(d.action === 'proceed-with-warning' && d.message).toContain('do NOT run a release');
});
it('refuses a saving tool when no pin is available', () => {
const d = decideVersionPin(undefined, { sp: 21, patch: 5 }, { saves: true, projectFilePath });
expect(d.action).toBe('refuse');
expect(d.action === 'refuse' && d.message).toContain('.codesys-version');
});
it('lets an unpinned read-only tool through, so existing repos keep working', () => {
expect(
decideVersionPin(undefined, { sp: 21, patch: 5 }, { saves: false, projectFilePath })
).toEqual({ action: 'proceed' });
});
it('honours an explicit allowVersionUpgrade override', () => {
expect(
decideVersionPin(pin(19, 2), { sp: 21, patch: 5 }, {
saves: true,
allowUpgrade: true,
projectFilePath,
})
).toEqual({ action: 'proceed' });
});
it('stays out of the way when the server profile cannot be parsed', () => {
expect(
decideVersionPin(pin(19, 2), undefined, { saves: true, projectFilePath })
).toEqual({ action: 'proceed' });
});
});
describe('label', () => {
it('omits "Patch 0"', () => {
expect(label({ sp: 19, patch: 0 })).toBe('MSP-Con V3.5 SP19');
expect(label({ sp: 21, patch: 5 })).toBe('MSP-Con V3.5 SP21 Patch 5');
});
});