This commit was merged in pull request #802.
This commit is contained in:
@@ -1,9 +1,22 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from 'node:fs';
|
||||
import {
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
rmSync,
|
||||
readFileSync,
|
||||
existsSync,
|
||||
copyFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { FileConfigAdapter, DEFAULT_SEED_FILES } from './file-adapter.js';
|
||||
|
||||
// The real shipping manifest — the fixture uses it verbatim so these tests
|
||||
// exercise production ownership resolution, not a synthetic copy (#791).
|
||||
const REAL_FRAMEWORK_ROOT = fileURLToPath(new URL('../../framework', import.meta.url));
|
||||
|
||||
/**
|
||||
* Regression tests for the `FileConfigAdapter.syncFramework` seed behavior.
|
||||
*
|
||||
@@ -34,6 +47,13 @@ function makeFixture(): { sourceDir: string; mosaicHome: string; defaultsDir: st
|
||||
mkdirSync(defaultsDir, { recursive: true });
|
||||
mkdirSync(mosaicHome, { recursive: true });
|
||||
|
||||
// #791: syncFramework resolves ownership from the shared manifest under the
|
||||
// source dir. Seed the real one so keep-mode syncs behave as in production.
|
||||
copyFileSync(
|
||||
join(REAL_FRAMEWORK_ROOT, 'framework-manifest.txt'),
|
||||
join(sourceDir, 'framework-manifest.txt'),
|
||||
);
|
||||
|
||||
// Framework-contract defaults we expect the wizard to seed.
|
||||
writeFileSync(join(defaultsDir, 'CONSTITUTION.md'), '# CONSTITUTION default\n');
|
||||
writeFileSync(join(defaultsDir, 'AGENTS.md'), '# AGENTS default\n');
|
||||
@@ -102,9 +122,10 @@ describe('FileConfigAdapter.syncFramework — defaults seeding', () => {
|
||||
});
|
||||
|
||||
it('overwrites framework-owned files (backup-once) but preserves user-seeded files', async () => {
|
||||
// Plant a root-level AGENTS.md in sourceDir so syncDirectory's preserve is exercised.
|
||||
writeFileSync(join(fixture.sourceDir, 'AGENTS.md'), '# shipped AGENTS from source root\n');
|
||||
|
||||
// Contract files (CONSTITUTION/AGENTS/STANDARDS) ship only under defaults/ —
|
||||
// reconcile_framework_files is their sole writer (backup-once). The bulk
|
||||
// sync never sees a root-level copy, so a user's edited root file is backed
|
||||
// up, not silently clobbered, on upgrade.
|
||||
writeFileSync(join(fixture.mosaicHome, 'TOOLS.md'), '# user-customized TOOLS\n');
|
||||
writeFileSync(join(fixture.mosaicHome, 'AGENTS.md'), '# user-customized AGENTS\n');
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
buildToolsTemplateVars,
|
||||
} from '../template/builders.js';
|
||||
import { atomicWrite, backupFile, syncDirectory } from '../platform/file-ops.js';
|
||||
import { loadManifest, resolveOwnership } from '../framework/manifest.js';
|
||||
|
||||
/**
|
||||
* Parse a SoulConfig from an existing SOUL.md file.
|
||||
@@ -155,38 +156,22 @@ export class FileConfigAdapter implements ConfigService {
|
||||
}
|
||||
|
||||
async syncFramework(action: InstallAction): Promise<void> {
|
||||
// Must match PRESERVE_PATHS in packages/mosaic/framework/install.sh so
|
||||
// the bash and TS install paths have the same upgrade-preservation
|
||||
// semantics. Contract files (AGENTS.md, STANDARDS.md, TOOLS.md) are
|
||||
// seeded from defaults/ on first install and preserved thereafter;
|
||||
// identity files (SOUL.md, USER.md) are generated by wizard stages and
|
||||
// must never be touched by the framework sync.
|
||||
const preservePaths =
|
||||
action === 'keep' || action === 'reconfigure'
|
||||
? [
|
||||
'CONSTITUTION.md',
|
||||
'AGENTS.md',
|
||||
'SOUL.md',
|
||||
'USER.md',
|
||||
'TOOLS.md',
|
||||
'STANDARDS.md',
|
||||
'memory',
|
||||
'sources',
|
||||
'credentials',
|
||||
// User-authored fleet data MUST survive `mosaic update`'s re-seed.
|
||||
// The framework seeds only fleet/examples + fleet/roles +
|
||||
// fleet/roster.schema.json; the operator's roster, per-agent env, and
|
||||
// heartbeat run dir stay user-owned. (Mirror of install.sh PRESERVE_PATHS.)
|
||||
'fleet/roster.yaml',
|
||||
'fleet/roster.json',
|
||||
'fleet/agents',
|
||||
'fleet/run',
|
||||
]
|
||||
: [];
|
||||
// #791: ownership is derived from the shared framework manifest
|
||||
// (packages/mosaic/framework/framework-manifest.txt) — the SAME file the
|
||||
// bash installer reads — so the TS and bash paths can never drift. On an
|
||||
// upgrade (keep/reconfigure) the sync must NEVER write an operator-owned
|
||||
// path: every operator file, and any path the manifest never anticipated
|
||||
// (which resolves to operator by the fail-safe default), is left untouched.
|
||||
// A fresh install ('overwrite'/'reconfigure' onto an empty home) seeds the
|
||||
// full tree, so the guard applies only when preserving an existing home.
|
||||
const guardOwnership = action === 'keep' || action === 'reconfigure';
|
||||
const manifest = guardOwnership ? loadManifest(this.sourceDir) : undefined;
|
||||
|
||||
syncDirectory(this.sourceDir, this.mosaicHome, {
|
||||
preserve: preservePaths,
|
||||
excludeGit: true,
|
||||
isOperatorOwned: manifest
|
||||
? (relPath) => resolveOwnership(manifest, relPath) === 'operator'
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Reconcile framework-contract files from framework/defaults/ into the mosaic
|
||||
|
||||
Reference in New Issue
Block a user