168 lines
6.1 KiB
TypeScript
168 lines
6.1 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import {
|
|
createManifest,
|
|
readManifest,
|
|
writeManifest,
|
|
manifestPath,
|
|
heuristicRuntimeAssetDests,
|
|
DEFAULT_SCOPE_LINE,
|
|
MANIFEST_VERSION,
|
|
} from './install-manifest.js';
|
|
|
|
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-manifest-test-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// ─── createManifest ───────────────────────────────────────────────────────────
|
|
|
|
describe('createManifest', () => {
|
|
it('creates a valid manifest with version 1', () => {
|
|
const m = createManifest('0.0.24', 2);
|
|
expect(m.version).toBe(MANIFEST_VERSION);
|
|
expect(m.cliVersion).toBe('0.0.24');
|
|
expect(m.frameworkVersion).toBe(2);
|
|
});
|
|
|
|
it('sets installedAt to an ISO-8601 date string', () => {
|
|
const before = new Date();
|
|
const m = createManifest('0.0.24', 2);
|
|
const after = new Date();
|
|
const ts = new Date(m.installedAt);
|
|
expect(ts.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
expect(ts.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
});
|
|
|
|
it('starts with empty mutation arrays', () => {
|
|
const m = createManifest('0.0.24', 2);
|
|
expect(m.mutations.directories).toHaveLength(0);
|
|
expect(m.mutations.npmGlobalPackages).toHaveLength(0);
|
|
expect(m.mutations.npmrcLines).toHaveLength(0);
|
|
expect(m.mutations.shellProfileEdits).toHaveLength(0);
|
|
expect(m.mutations.runtimeAssetCopies).toHaveLength(0);
|
|
});
|
|
|
|
it('merges partial mutations', () => {
|
|
const m = createManifest('0.0.24', 2, {
|
|
npmGlobalPackages: ['@mosaicstack/mosaic'],
|
|
});
|
|
expect(m.mutations.npmGlobalPackages).toEqual(['@mosaicstack/mosaic']);
|
|
expect(m.mutations.directories).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ─── manifestPath ─────────────────────────────────────────────────────────────
|
|
|
|
describe('manifestPath', () => {
|
|
it('returns mosaicHome/.install-manifest.json', () => {
|
|
const p = manifestPath('/home/user/.config/mosaic');
|
|
expect(p).toBe('/home/user/.config/mosaic/.install-manifest.json');
|
|
});
|
|
});
|
|
|
|
// ─── writeManifest / readManifest round-trip ─────────────────────────────────
|
|
|
|
describe('writeManifest + readManifest', () => {
|
|
it('round-trips a manifest through disk', () => {
|
|
const m = createManifest('0.0.24', 2, {
|
|
npmGlobalPackages: ['@mosaicstack/mosaic'],
|
|
npmrcLines: [DEFAULT_SCOPE_LINE],
|
|
});
|
|
|
|
writeManifest(tmpDir, m);
|
|
const loaded = readManifest(tmpDir);
|
|
|
|
expect(loaded).toBeDefined();
|
|
expect(loaded!.version).toBe(1);
|
|
expect(loaded!.cliVersion).toBe('0.0.24');
|
|
expect(loaded!.mutations.npmGlobalPackages).toEqual(['@mosaicstack/mosaic']);
|
|
expect(loaded!.mutations.npmrcLines).toEqual([DEFAULT_SCOPE_LINE]);
|
|
});
|
|
|
|
it('preserves runtimeAssetCopies with backup path', () => {
|
|
const m = createManifest('0.0.24', 2, {
|
|
runtimeAssetCopies: [
|
|
{
|
|
source: '/src/settings.json',
|
|
dest: '/home/user/.claude/settings.json',
|
|
backup: '/home/user/.claude/settings.json.mosaic-bak-20260405120000',
|
|
},
|
|
],
|
|
});
|
|
|
|
writeManifest(tmpDir, m);
|
|
const loaded = readManifest(tmpDir);
|
|
|
|
const copies = loaded!.mutations.runtimeAssetCopies;
|
|
expect(copies).toHaveLength(1);
|
|
expect(copies[0]!.backup).toBe('/home/user/.claude/settings.json.mosaic-bak-20260405120000');
|
|
});
|
|
});
|
|
|
|
// ─── readManifest — missing / invalid ────────────────────────────────────────
|
|
|
|
describe('readManifest error cases', () => {
|
|
it('returns undefined when the file does not exist', () => {
|
|
expect(readManifest('/nonexistent/path')).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined when the file contains invalid JSON', () => {
|
|
const { writeFileSync } = require('node:fs');
|
|
writeFileSync(join(tmpDir, '.install-manifest.json'), 'not json', 'utf8');
|
|
expect(readManifest(tmpDir)).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined when version field is wrong', () => {
|
|
const { writeFileSync } = require('node:fs');
|
|
writeFileSync(
|
|
join(tmpDir, '.install-manifest.json'),
|
|
JSON.stringify({
|
|
version: 99,
|
|
installedAt: new Date().toISOString(),
|
|
cliVersion: '1',
|
|
frameworkVersion: 1,
|
|
mutations: {},
|
|
}),
|
|
'utf8',
|
|
);
|
|
expect(readManifest(tmpDir)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ─── heuristicRuntimeAssetDests ──────────────────────────────────────────────
|
|
|
|
describe('heuristicRuntimeAssetDests', () => {
|
|
it('returns a non-empty list of absolute paths', () => {
|
|
const dests = heuristicRuntimeAssetDests('/home/user');
|
|
expect(dests.length).toBeGreaterThan(0);
|
|
for (const d of dests) {
|
|
expect(d).toMatch(/^\/home\/user\//);
|
|
}
|
|
});
|
|
|
|
it('includes the claude settings.json path', () => {
|
|
const dests = heuristicRuntimeAssetDests('/home/user');
|
|
expect(dests).toContain('/home/user/.claude/settings.json');
|
|
});
|
|
});
|
|
|
|
// ─── DEFAULT_SCOPE_LINE ───────────────────────────────────────────────────────
|
|
|
|
describe('DEFAULT_SCOPE_LINE', () => {
|
|
it('contains the mosaicstack registry URL', () => {
|
|
expect(DEFAULT_SCOPE_LINE).toContain('mosaicstack');
|
|
expect(DEFAULT_SCOPE_LINE).toContain('@mosaicstack:registry=');
|
|
});
|
|
});
|