Files
stack/packages/mosaic/src/runtime/update-checker.reseed.spec.ts
jason.woltje adad486b6f
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
fix(fleet): enforce exact comms authority (#787)
2026-07-16 00:32:23 +00:00

419 lines
16 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
chmodSync,
existsSync,
lstatSync,
mkdtempSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
buildReseedCommand,
buildRelaunchCommands,
readRosterAgentNames,
runFrameworkReseed,
refreshActiveFleetUnits,
readInstalledFrameworkVersion,
readBundledFrameworkVersion,
checkFrameworkDrift,
repairFleetCommsTools,
} from './update-checker.js';
/**
* F3-m3 / R13: `mosaic update` re-seeds the framework + (opt-in) relaunches
* durable agents so shipped launcher/runtime changes activate. These cover the
* pure builders + the missing-installer guard (the exec path is integration).
*/
describe('buildReseedCommand', () => {
it('invokes the package install.sh in data-safe sync-only keep mode', () => {
const out = buildReseedCommand('/pkg/framework', '/home/u/.config/mosaic');
expect(out.installer).toBe('/pkg/framework/install.sh');
expect(out.command).toBe('bash /pkg/framework/install.sh');
expect(out.env).toEqual({
MOSAIC_SYNC_ONLY: '1',
MOSAIC_INSTALL_MODE: 'keep',
MOSAIC_HOME: '/home/u/.config/mosaic',
});
});
});
describe('buildRelaunchCommands', () => {
it('builds a systemctl --user restart per agent unit', () => {
expect(buildRelaunchCommands(['orchestrator', 'coder0'])).toEqual([
['systemctl', '--user', 'restart', 'mosaic-agent@orchestrator.service'],
['systemctl', '--user', 'restart', 'mosaic-agent@coder0.service'],
]);
});
it('is empty for an empty roster', () => {
expect(buildRelaunchCommands([])).toEqual([]);
});
});
describe('readRosterAgentNames', () => {
let home: string;
beforeEach(() => {
home = mkdtempSync(join(tmpdir(), 'mosaic-roster-'));
});
afterEach(() => {
rmSync(home, { recursive: true, force: true });
});
it('returns [] when no roster exists', () => {
expect(readRosterAgentNames(home)).toEqual([]);
});
it('extracts agent names from roster.yaml', () => {
mkdirSync(join(home, 'fleet'), { recursive: true });
writeFileSync(
join(home, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'agents:',
' - name: orchestrator',
' runtime: pi',
' - name: coder0',
' runtime: claude',
' - name: "reviewer-1"',
' runtime: codex',
].join('\n') + '\n',
);
expect(readRosterAgentNames(home)).toEqual(['orchestrator', 'coder0', 'reviewer-1']);
});
it('extracts agent names from a JSON-only roster', () => {
mkdirSync(join(home, 'fleet'), { recursive: true });
writeFileSync(
join(home, 'fleet', 'roster.json'),
JSON.stringify({
version: 1,
transport: 'tmux',
agents: [
{ name: 'orchestrator', runtime: 'pi', class: 'orchestrator' },
{ name: 'coder0', runtime: 'claude', class: 'worker' },
],
}),
);
expect(readRosterAgentNames(home)).toEqual(['orchestrator', 'coder0']);
});
});
describe('repairFleetCommsTools', () => {
let root: string;
let framework: string;
let home: string;
const toolsContent = '# tools\n<!-- fleet-comms-contract: 1 -->\n';
const helperContent = '#!/bin/sh\nexit 0\n';
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'mosaic-tools-repair-'));
framework = join(root, 'framework');
home = join(root, 'home');
mkdirSync(join(framework, 'defaults'), { recursive: true });
mkdirSync(join(framework, 'tools', 'tmux'), { recursive: true });
writeFileSync(join(framework, 'defaults', 'TOOLS.md'), toolsContent);
const helper = join(framework, 'tools', 'tmux', 'agent-send.sh');
writeFileSync(helper, helperContent);
chmodSync(helper, 0o755);
});
afterEach(() => rmSync(root, { recursive: true, force: true }));
it('restores a partially deleted current-version installation without package updates', () => {
mkdirSync(join(home, 'tools', 'tmux'), { recursive: true });
writeFileSync(join(home, 'TOOLS.md'), toolsContent);
const result = repairFleetCommsTools(framework, home);
expect(result).toMatchObject({ ok: true, changed: true });
expect(readFileSync(join(home, 'TOOLS.md'), 'utf8')).toBe(toolsContent);
expect(readFileSync(join(home, 'tools', 'tmux', 'agent-send.sh'), 'utf8')).toBe(helperContent);
expect(lstatSync(join(home, 'tools', 'tmux', 'agent-send.sh')).mode & 0o111).not.toBe(0);
});
it('creates a digest-qualified no-clobber backup and is idempotent', () => {
mkdirSync(home, { recursive: true });
const stale = '# user tools\n';
writeFileSync(join(home, 'TOOLS.md'), stale);
const first = repairFleetCommsTools(framework, home);
expect(first).toMatchObject({ ok: true, changed: true });
expect(first.backupPath).toMatch(/\.pre-fleet-comms-[a-f0-9]{16}\.bak$/);
expect(readFileSync(first.backupPath!, 'utf8')).toBe(stale);
const second = repairFleetCommsTools(framework, home);
expect(second).toEqual({ ok: true, changed: false, backupPath: undefined });
expect(readFileSync(first.backupPath!, 'utf8')).toBe(stale);
});
it('rejects an installed helper symlink without modifying its target', () => {
mkdirSync(join(home, 'tools', 'tmux'), { recursive: true });
writeFileSync(join(home, 'TOOLS.md'), toolsContent);
const target = join(root, 'external-helper');
writeFileSync(target, 'do not touch\n');
symlinkSync(target, join(home, 'tools', 'tmux', 'agent-send.sh'));
const result = repairFleetCommsTools(framework, home);
expect(result).toMatchObject({ ok: false, changed: false });
expect(result.reason).toContain('symbolic link');
expect(readFileSync(target, 'utf8')).toBe('do not touch\n');
expect(lstatSync(join(home, 'tools', 'tmux', 'agent-send.sh')).isSymbolicLink()).toBe(true);
});
it('refuses a helper directory before replacing stale TOOLS content', () => {
mkdirSync(join(home, 'tools', 'tmux', 'agent-send.sh'), { recursive: true });
const stale = '# user tools\n';
writeFileSync(join(home, 'TOOLS.md'), stale);
const result = repairFleetCommsTools(framework, home);
expect(result).toMatchObject({ ok: false, changed: false });
expect(result.reason).toContain('not a regular file');
expect(readFileSync(join(home, 'TOOLS.md'), 'utf8')).toBe(stale);
});
it('refuses a pre-existing digest backup whose bytes do not match', () => {
mkdirSync(home, { recursive: true });
const stale = '# user tools\n';
writeFileSync(join(home, 'TOOLS.md'), stale);
const digest = createHash('sha256').update(stale).digest('hex').slice(0, 16);
writeFileSync(join(home, `TOOLS.md.pre-fleet-comms-${digest}.bak`), 'collision\n');
const result = repairFleetCommsTools(framework, home);
expect(result).toMatchObject({ ok: false, changed: false });
expect(result.reason).toContain('backup collision');
expect(readFileSync(join(home, 'TOOLS.md'), 'utf8')).toBe(stale);
});
it('rejects a symlink in each installed destination ancestor without external writes', () => {
const cases = [
{ name: 'home', prefix: join(root, 'linked-home'), suffix: '' },
{ name: 'tools', prefix: join(root, 'real-home'), suffix: 'tools' },
{ name: 'tmux', prefix: join(root, 'real-home'), suffix: join('tools', 'tmux') },
];
for (const testCase of cases) {
const external = join(root, `external-${testCase.name}`);
mkdirSync(external, { recursive: true });
const targetHome =
testCase.name === 'home' ? testCase.prefix : join(root, `installed-${testCase.name}`);
if (testCase.name === 'home') {
symlinkSync(external, targetHome);
} else {
mkdirSync(targetHome, { recursive: true });
const linkPath = join(targetHome, testCase.suffix);
mkdirSync(join(linkPath, '..'), { recursive: true });
symlinkSync(external, linkPath);
}
const result = repairFleetCommsTools(framework, targetHome);
expect(result, testCase.name).toMatchObject({ ok: false, changed: false });
expect(result.reason, testCase.name).toContain('symbolic link');
expect(readdirSync(external), testCase.name).toEqual([]);
}
});
it('rolls back the backup and exact TOOLS bytes/mode when helper commit fails', () => {
mkdirSync(join(home, 'tools', 'tmux'), { recursive: true });
const staleTools = '# user tools\n';
const staleHelper = '#!/bin/sh\nexit 17\n';
writeFileSync(join(home, 'TOOLS.md'), staleTools, { mode: 0o640 });
writeFileSync(join(home, 'tools', 'tmux', 'agent-send.sh'), staleHelper, { mode: 0o710 });
const result = repairFleetCommsTools(framework, home, {
beforeCommit(which) {
if (which === 'helper') throw new Error('injected helper commit failure');
},
});
expect(result).toMatchObject({ ok: false, changed: false });
expect(result.backupPath).toBeUndefined();
expect(result.reason).toContain('injected helper commit failure');
expect(readFileSync(join(home, 'TOOLS.md'), 'utf8')).toBe(staleTools);
expect(statSync(join(home, 'TOOLS.md')).mode & 0o777).toBe(0o640);
expect(readFileSync(join(home, 'tools', 'tmux', 'agent-send.sh'), 'utf8')).toBe(staleHelper);
expect(statSync(join(home, 'tools', 'tmux', 'agent-send.sh')).mode & 0o777).toBe(0o710);
expect(readdirSync(home).filter((name) => name.includes('pre-fleet-comms'))).toEqual([]);
expect(
readdirSync(home).some((name) => name.includes('.repair-')) ||
readdirSync(join(home, 'tools', 'tmux')).some((name) => name.includes('.repair-')),
).toBe(false);
});
it('rolls back initially absent destinations and created directories on commit failure', () => {
const result = repairFleetCommsTools(framework, home, {
beforeCommit(which) {
if (which === 'helper') throw new Error('injected absent helper failure');
},
});
expect(result).toMatchObject({ ok: false, changed: false });
expect(result.reason).toContain('injected absent helper failure');
expect(existsSync(home)).toBe(false);
});
it('does not persist a backup or replacement when backup commit fails', () => {
mkdirSync(home, { recursive: true });
const stale = '# user tools\n';
writeFileSync(join(home, 'TOOLS.md'), stale, { mode: 0o640 });
const result = repairFleetCommsTools(framework, home, {
beforeCommit(which) {
if (which === 'backup') throw new Error('injected backup commit failure');
},
});
expect(result).toMatchObject({ ok: false, changed: false });
expect(readFileSync(join(home, 'TOOLS.md'), 'utf8')).toBe(stale);
expect(statSync(join(home, 'TOOLS.md')).mode & 0o777).toBe(0o640);
expect(readdirSync(home).filter((name) => name.includes('pre-fleet-comms'))).toEqual([]);
});
it('fails before writes when bundled source paths traverse a symlink ancestor', () => {
const external = join(root, 'external-source');
mkdirSync(join(external, 'defaults'), { recursive: true });
mkdirSync(join(external, 'tools', 'tmux'), { recursive: true });
writeFileSync(join(external, 'defaults', 'TOOLS.md'), toolsContent);
writeFileSync(join(external, 'tools', 'tmux', 'agent-send.sh'), helperContent, { mode: 0o755 });
const linkedFramework = join(root, 'linked-framework');
symlinkSync(external, linkedFramework);
const result = repairFleetCommsTools(linkedFramework, home);
expect(result).toMatchObject({ ok: false, changed: false });
expect(result.reason).toContain('symbolic link');
expect(existsSync(home)).toBe(false);
});
});
describe('runFrameworkReseed', () => {
it('reports not-ok (not throw) when the installer is absent', () => {
const missing = mkdtempSync(join(tmpdir(), 'mosaic-noinstaller-'));
const res = runFrameworkReseed(missing, join(missing, 'home'));
expect(res.ok).toBe(false);
expect(res.reason).toContain('installer not found');
rmSync(missing, { recursive: true, force: true });
});
});
describe('refreshActiveFleetUnits', () => {
let root: string;
let mosaicHome: string;
let configHome: string;
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'mosaic-units-'));
mosaicHome = join(root, 'mosaic');
configHome = join(root, 'config');
mkdirSync(join(mosaicHome, 'systemd', 'user'), { recursive: true });
mkdirSync(join(configHome, 'systemd', 'user'), { recursive: true });
// Freshly re-seeded units (new content).
writeFileSync(join(mosaicHome, 'systemd', 'user', 'mosaic-agent@.service'), 'NEW\n');
writeFileSync(join(mosaicHome, 'systemd', 'user', 'mosaic-tmux-holder.service'), 'NEW\n');
});
afterEach(() => rmSync(root, { recursive: true, force: true }));
it('refreshes active units when a fleet is already installed', () => {
// Active dir already carries mosaic units (stale) → fleet is installed.
writeFileSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'), 'OLD\n');
const res = refreshActiveFleetUnits(mosaicHome, {
XDG_CONFIG_HOME: configHome,
} as NodeJS.ProcessEnv);
expect(res.refreshed).toContain('mosaic-agent@.service');
expect(
readFileSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'), 'utf-8'),
).toBe('NEW\n');
});
it('is a no-op when no fleet is installed (active dir has no mosaic units)', () => {
const res = refreshActiveFleetUnits(mosaicHome, {
XDG_CONFIG_HOME: configHome,
} as NodeJS.ProcessEnv);
expect(res.refreshed).toEqual([]);
expect(existsSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'))).toBe(false);
});
});
/**
* #642: re-seed when the on-disk framework is older than the bundled one even
* if no package is reported outdated (CLI upgraded outside `mosaic update`).
*/
describe('framework drift detection', () => {
let home: string; // stand-in for ~/.config/mosaic
let fw: string; // stand-in for the bundled framework root
beforeEach(() => {
const root = mkdtempSync(join(tmpdir(), 'mosaic-drift-'));
home = join(root, 'mosaic');
fw = join(root, 'framework');
mkdirSync(home, { recursive: true });
mkdirSync(fw, { recursive: true });
});
afterEach(() => {
rmSync(join(home, '..'), { recursive: true, force: true });
});
const writeInstalled = (v: string) => writeFileSync(join(home, '.framework-version'), v);
const writeBundled = (v: string) =>
writeFileSync(join(fw, 'install.sh'), `#!/usr/bin/env bash\nFRAMEWORK_VERSION=${v}\n`);
describe('readInstalledFrameworkVersion', () => {
it('returns undefined when the version file is absent', () => {
expect(readInstalledFrameworkVersion(home)).toBeUndefined();
});
it('parses the integer (tolerating surrounding whitespace)', () => {
writeInstalled(' 3\n');
expect(readInstalledFrameworkVersion(home)).toBe(3);
});
it('returns undefined for non-numeric content', () => {
writeInstalled('not-a-number\n');
expect(readInstalledFrameworkVersion(home)).toBeUndefined();
});
});
describe('readBundledFrameworkVersion', () => {
it('returns undefined when install.sh is absent', () => {
expect(readBundledFrameworkVersion(fw)).toBeUndefined();
});
it('parses FRAMEWORK_VERSION=<n> from install.sh', () => {
writeBundled('4');
expect(readBundledFrameworkVersion(fw)).toBe(4);
});
});
describe('checkFrameworkDrift', () => {
it('reports drift when on-disk is older than bundled', () => {
writeInstalled('3');
writeBundled('4');
expect(checkFrameworkDrift(home, fw)).toEqual({ drifted: true, installed: 3, bundled: 4 });
});
it('no drift when versions match', () => {
writeInstalled('4');
writeBundled('4');
expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false });
});
it('no drift when on-disk is newer than bundled', () => {
writeInstalled('5');
writeBundled('4');
expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false });
});
it('no drift (conservative) when a version cannot be read', () => {
writeBundled('4'); // installed version file missing
expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false, bundled: 4 });
});
});
});