fix(codex-auth): share plugin cache across profiles

This commit is contained in:
Tam Nhu Tran committed 2026-07-15 10:24:13 -04:00
1 parent 463a1ffa50
commit e5d4d860b3
3 files changed
+619 -1

No files matched your search

@@ -0,0 +1,219 @@
import * as fs from 'fs';
import * as path from 'path';
import { ConfigError } from '../errors/error-types';
import { symlinkPointsTo } from '../management/shared-manager/fs-helpers';
import { createLogger } from '../services/logging';
const logger = createLogger('codex-auth:resources');
const FALLBACK_SAFE_SYMLINK_ERRORS = new Set(['EPERM', 'EACCES', 'ENOSYS']);
export function ensureSharedPluginCache(profileDir: string, sharedCodexHome: string): void {
const targetPath = path.join(sharedCodexHome, 'plugins', 'cache');
const pluginsPath = path.join(profileDir, 'plugins');
const linkPath = path.join(pluginsPath, 'cache');
ensureProfileLocalPluginsDirectory(pluginsPath);
fs.mkdirSync(targetPath, { recursive: true, mode: 0o700 });
const existingStat = lstatIfExists(linkPath);
if (isExpectedCacheLink(linkPath, targetPath, existingStat)) {
return;
}
let backupPath = existingStat === null ? null : createPluginCacheBackupPath(pluginsPath);
if (backupPath !== null) {
try {
fs.renameSync(linkPath, backupPath);
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err;
}
backupPath = null;
}
}
try {
fs.symlinkSync(targetPath, linkPath, 'dir');
} catch (err) {
if (isExpectedCacheLink(linkPath, targetPath)) {
removePluginCacheBackup(backupPath);
return;
}
const pathAfterFailure = lstatIfExists(linkPath);
if (backupPath !== null && pathAfterFailure === null) {
if (restorePluginCacheWithoutOverwrite(linkPath, backupPath)) {
backupPath = null;
}
}
const code = (err as NodeJS.ErrnoException | null)?.code;
if (!code || !FALLBACK_SAFE_SYMLINK_ERRORS.has(code)) {
throw err;
}
if (isExpectedCacheLink(linkPath, targetPath)) {
removePluginCacheBackup(backupPath);
return;
}
const fallbackPathStat = lstatIfExists(linkPath);
if (backupPath !== null && fallbackPathStat?.isDirectory()) {
mergeMissingResourceTree(backupPath, linkPath);
removePluginCacheBackup(backupPath);
}
ensureLocalPluginCacheDirectory(linkPath);
mergeMissingResourceTree(targetPath, linkPath);
process.stderr.write(
`[!] codex-auth: symlink unavailable; using profile-local plugin cache at ${linkPath}. ` +
`Copied missing shared entries; plugin cache updates won't propagate automatically.\n`
);
logger.warn(
'codex-auth.plugin-cache-copy-fallback',
'Copied shared plugin cache after symlink failure',
{
link: linkPath,
target: targetPath,
error: err instanceof Error ? err.message : String(err),
}
);
return;
}
removePluginCacheBackup(backupPath);
logger.stage(
'dispatch',
'codex.plugin-cache.symlink.created',
'Created shared plugin cache symlink',
{
link: linkPath,
target: targetPath,
}
);
}
function ensureProfileLocalPluginsDirectory(pluginsPath: string): void {
const pluginsStat = lstatIfExists(pluginsPath);
if (pluginsStat === null) {
fs.mkdirSync(pluginsPath, { recursive: true, mode: 0o700 });
return;
}
if (!pluginsStat.isDirectory()) {
throw new ConfigError(
'Refusing plugin cache repair: profile plugins path is not a local directory.'
);
}
}
function isExpectedCacheLink(
linkPath: string,
targetPath: string,
stat: fs.Stats | null = lstatIfExists(linkPath)
): boolean {
if (!stat?.isSymbolicLink()) {
return false;
}
return symlinkPointsTo(linkPath, targetPath);
}
function lstatIfExists(resourcePath: string): fs.Stats | null {
try {
return fs.lstatSync(resourcePath);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw err;
}
}
function createPluginCacheBackupPath(pluginsPath: string): string {
return path.join(
pluginsPath,
`.cache.ccs-backup-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
);
}
function removePluginCacheBackup(backupPath: string | null): void {
if (backupPath !== null) {
fs.rmSync(backupPath, { recursive: true, force: true });
}
}
function ensureLocalPluginCacheDirectory(linkPath: string): void {
let existingStat = lstatIfExists(linkPath);
if (existingStat?.isDirectory()) {
return;
}
if (existingStat !== null) {
throw new ConfigError(
'Refusing plugin cache fallback: profile cache path is not a local directory.'
);
}
try {
fs.mkdirSync(linkPath, { recursive: false, mode: 0o700 });
return;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
throw err;
}
}
existingStat = lstatIfExists(linkPath);
if (!existingStat?.isDirectory()) {
throw new ConfigError(
'Refusing plugin cache fallback: profile cache path is not a local directory.'
);
}
}
function restorePluginCacheWithoutOverwrite(linkPath: string, backupPath: string): boolean {
const backupStat = lstatIfExists(backupPath);
if (!backupStat?.isDirectory()) {
return false;
}
let linkStat = lstatIfExists(linkPath);
if (linkStat === null) {
try {
fs.mkdirSync(linkPath, { recursive: false, mode: 0o700 });
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
throw err;
}
}
linkStat = lstatIfExists(linkPath);
}
if (!linkStat?.isDirectory()) {
return false;
}
mergeMissingResourceTree(backupPath, linkPath);
removePluginCacheBackup(backupPath);
return true;
}
function mergeMissingResourceTree(sourceDir: string, targetDir: string): void {
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
const sourcePath = path.join(sourceDir, entry.name);
const targetPath = path.join(targetDir, entry.name);
const targetStat = lstatIfExists(targetPath);
if (targetStat === null) {
fs.cpSync(sourcePath, targetPath, {
recursive: true,
force: false,
errorOnExist: false,
preserveTimestamps: true,
});
continue;
}
if (entry.isDirectory() && targetStat.isDirectory()) {
mergeMissingResourceTree(sourcePath, targetPath);
}
}
}
@@ -2,6 +2,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { createLogger } from '../services/logging';
import { ensureSharedPluginCache } from './codex-profile-plugin-cache';
const logger = createLogger('codex-auth:resources');
@@ -23,6 +24,8 @@ export function ensureCodexProfileResources(
for (const resourceName of SHARED_CODEX_RESOURCE_DIRS) {
ensureSharedResourceDir(profileDir, sharedCodexHome, resourceName);
}
ensureSharedPluginCache(profileDir, sharedCodexHome);
}
function ensureSharedResourceDir(
@@ -6,6 +6,22 @@ import * as path from 'path';
let tempDir: string;
let profileDir: string;
let sharedCodexHome: string;
const cachedSkillRelativePath = path.join(
'openai-bundled',
'sites',
'0.1.27',
'skills',
'site-builder',
'SKILL.md'
);
const staleCachedSkillRelativePath = path.join(
'openai-bundled',
'sites',
'0.1.26',
'skills',
'site-builder',
'SKILL.md'
);
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resources-test-'));
@@ -18,6 +34,17 @@ beforeEach(() => {
);
fs.mkdirSync(path.join(sharedCodexHome, 'skills'), { recursive: true, mode: 0o700 });
fs.writeFileSync(path.join(sharedCodexHome, 'skills', 'review.md'), '# Review\n');
fs.mkdirSync(
path.join(sharedCodexHome, 'plugins', 'cache', path.dirname(cachedSkillRelativePath)),
{
recursive: true,
mode: 0o700,
}
);
fs.writeFileSync(
path.join(sharedCodexHome, 'plugins', 'cache', cachedSkillRelativePath),
'# Current shared skill\n'
);
});
afterEach(() => {
@@ -26,7 +53,7 @@ afterEach(() => {
});
describe('ensureCodexProfileResources', () => {
it('links shared agents and skills into a fresh Codex profile', async () => {
it('exposes shared agents, skills, and plugin cache in a fresh Codex profile', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
@@ -38,6 +65,291 @@ describe('ensureCodexProfileResources', () => {
expect(fs.lstatSync(resourcePath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(resourcePath)).toBe(path.join(sharedCodexHome, resourceName));
}
const cachedSkillPath = path.join(profileDir, 'plugins', 'cache', cachedSkillRelativePath);
expect(fs.readFileSync(cachedSkillPath, 'utf8')).toBe('# Current shared skill\n');
});
it('replaces a stale profile-local plugin cache while preserving plugin siblings', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
const pluginsDir = path.join(profileDir, 'plugins');
const cacheDir = path.join(pluginsDir, 'cache');
const staleSkillPath = path.join(cacheDir, staleCachedSkillRelativePath);
const currentSkillPath = path.join(cacheDir, cachedSkillRelativePath);
const siblingPath = path.join(pluginsDir, 'marketplaces.json');
fs.mkdirSync(path.dirname(staleSkillPath), { recursive: true, mode: 0o700 });
fs.writeFileSync(staleSkillPath, '# Stale profile skill\n');
fs.writeFileSync(siblingPath, '{"preserve":true}\n');
ensureCodexProfileResources(profileDir, { sharedCodexHome });
expect(fs.lstatSync(cacheDir).isSymbolicLink()).toBe(true);
expect(fs.existsSync(staleSkillPath)).toBe(false);
expect(fs.readFileSync(currentSkillPath, 'utf8')).toBe('# Current shared skill\n');
expect(fs.readFileSync(siblingPath, 'utf8')).toBe('{"preserve":true}\n');
});
it('refuses a symlinked plugins parent without changing its external target', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
const sharedPluginsDir = path.join(sharedCodexHome, 'plugins');
const sharedSkillPath = path.join(sharedCodexHome, 'plugins', 'cache', cachedSkillRelativePath);
const profilePluginsDir = path.join(profileDir, 'plugins');
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
fs.symlinkSync(sharedPluginsDir, profilePluginsDir, 'dir');
expect(() => ensureCodexProfileResources(profileDir, { sharedCodexHome })).toThrow(
'profile plugins path is not a local directory'
);
expect(fs.lstatSync(profilePluginsDir).isSymbolicLink()).toBe(true);
expect(fs.lstatSync(path.join(sharedPluginsDir, 'cache')).isDirectory()).toBe(true);
expect(fs.readFileSync(sharedSkillPath, 'utf8')).toBe('# Current shared skill\n');
});
it('keeps the plugin cache projection stable across repeated repairs', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
ensureCodexProfileResources(profileDir, { sharedCodexHome });
const cacheDir = path.join(profileDir, 'plugins', 'cache');
const firstTarget = fs.readlinkSync(cacheDir);
const firstInode = fs.lstatSync(cacheDir).ino;
ensureCodexProfileResources(profileDir, { sharedCodexHome });
expect(fs.readlinkSync(cacheDir)).toBe(firstTarget);
expect(fs.lstatSync(cacheDir).ino).toBe(firstInode);
expect(fs.readFileSync(path.join(cacheDir, cachedSkillRelativePath), 'utf8')).toBe(
'# Current shared skill\n'
);
});
it('keeps a concurrently-created correct cache link instead of restoring stale state', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
const cacheDir = path.join(profileDir, 'plugins', 'cache');
const staleSkillPath = path.join(cacheDir, staleCachedSkillRelativePath);
fs.mkdirSync(path.dirname(staleSkillPath), { recursive: true, mode: 0o700 });
fs.writeFileSync(staleSkillPath, '# Stale profile skill\n');
const realSymlinkSync = fs.symlinkSync.bind(fs);
let injectedConcurrentRepair = false;
const symlinkSpy = spyOn(fs, 'symlinkSync').mockImplementation(
(...args: Parameters<typeof fs.symlinkSync>) => {
const [, pathToCreate] = args;
if (
!injectedConcurrentRepair &&
path.resolve(String(pathToCreate)) === path.resolve(cacheDir)
) {
injectedConcurrentRepair = true;
ensureCodexProfileResources(profileDir, { sharedCodexHome });
}
return realSymlinkSync(...args);
}
);
try {
ensureCodexProfileResources(profileDir, { sharedCodexHome });
} finally {
symlinkSpy.mockRestore();
}
expect(fs.lstatSync(cacheDir).isSymbolicLink()).toBe(true);
expect(fs.existsSync(staleSkillPath)).toBe(false);
expect(fs.readFileSync(path.join(cacheDir, cachedSkillRelativePath), 'utf8')).toBe(
'# Current shared skill\n'
);
expect(
fs.readdirSync(path.dirname(cacheDir)).some((name) => name.startsWith('.cache.ccs-backup-'))
).toBe(false);
});
it('recovers when another repair moves the stale cache before this repair can rename it', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
const cacheDir = path.join(profileDir, 'plugins', 'cache');
const staleSkillPath = path.join(cacheDir, staleCachedSkillRelativePath);
fs.mkdirSync(path.dirname(staleSkillPath), { recursive: true, mode: 0o700 });
fs.writeFileSync(staleSkillPath, '# Stale profile skill\n');
const realRenameSync = fs.renameSync.bind(fs);
let injectedRenameRace = false;
const renameSpy = spyOn(fs, 'renameSync').mockImplementation(
(...args: Parameters<typeof fs.renameSync>) => {
const [oldPath] = args;
if (!injectedRenameRace && path.resolve(String(oldPath)) === path.resolve(cacheDir)) {
injectedRenameRace = true;
fs.rmSync(cacheDir, { recursive: true, force: true });
throw Object.assign(new Error('simulated concurrent move'), { code: 'ENOENT' });
}
return realRenameSync(...args);
}
);
try {
ensureCodexProfileResources(profileDir, { sharedCodexHome });
} finally {
renameSpy.mockRestore();
}
expect(fs.lstatSync(cacheDir).isSymbolicLink()).toBe(true);
expect(fs.existsSync(staleSkillPath)).toBe(false);
expect(fs.readFileSync(path.join(cacheDir, cachedSkillRelativePath), 'utf8')).toBe(
'# Current shared skill\n'
);
});
it('does not delete a concurrent cache path when symlink creation loses an EEXIST race', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
const pluginsDir = path.join(profileDir, 'plugins');
const cacheDir = path.join(pluginsDir, 'cache');
const staleSkillPath = path.join(cacheDir, staleCachedSkillRelativePath);
const concurrentMarkerPath = path.join(cacheDir, 'concurrent-marker.txt');
fs.mkdirSync(path.dirname(staleSkillPath), { recursive: true, mode: 0o700 });
fs.writeFileSync(staleSkillPath, '# Stale profile skill\n');
const realSymlinkSync = fs.symlinkSync.bind(fs);
const symlinkSpy = spyOn(fs, 'symlinkSync').mockImplementation(
(...args: Parameters<typeof fs.symlinkSync>) => {
const [, pathToCreate] = args;
if (path.resolve(String(pathToCreate)) === path.resolve(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
fs.writeFileSync(concurrentMarkerPath, 'preserve concurrent cache\n');
throw Object.assign(new Error('simulated concurrent EEXIST'), { code: 'EEXIST' });
}
return realSymlinkSync(...args);
}
);
try {
expect(() => ensureCodexProfileResources(profileDir, { sharedCodexHome })).toThrow(
'simulated concurrent EEXIST'
);
} finally {
symlinkSpy.mockRestore();
}
expect(fs.readFileSync(concurrentMarkerPath, 'utf8')).toBe('preserve concurrent cache\n');
const backupNames = fs
.readdirSync(pluginsDir)
.filter((name) => name.startsWith('.cache.ccs-backup-'));
expect(backupNames).toHaveLength(1);
expect(
fs.readFileSync(path.join(pluginsDir, backupNames[0], staleCachedSkillRelativePath), 'utf8')
).toBe('# Stale profile skill\n');
});
it('merges rollback data when a concurrent cache appears during restoration', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
const cacheDir = path.join(profileDir, 'plugins', 'cache');
const staleSkillPath = path.join(cacheDir, staleCachedSkillRelativePath);
const concurrentMarkerPath = path.join(cacheDir, 'concurrent-marker.txt');
fs.mkdirSync(path.dirname(staleSkillPath), { recursive: true, mode: 0o700 });
fs.writeFileSync(staleSkillPath, '# Stale profile skill\n');
const realSymlinkSync = fs.symlinkSync.bind(fs);
const symlinkSpy = spyOn(fs, 'symlinkSync').mockImplementation(
(...args: Parameters<typeof fs.symlinkSync>) => {
const [, pathToCreate] = args;
if (path.resolve(String(pathToCreate)) === path.resolve(cacheDir)) {
throw Object.assign(new Error('simulated filesystem corruption'), { code: 'EIO' });
}
return realSymlinkSync(...args);
}
);
const realMkdirSync = fs.mkdirSync.bind(fs);
let injectedRestoreRace = false;
const mkdirSpy = spyOn(fs, 'mkdirSync').mockImplementation(
(...args: Parameters<typeof fs.mkdirSync>) => {
const [pathToCreate] = args;
if (!injectedRestoreRace && path.resolve(String(pathToCreate)) === path.resolve(cacheDir)) {
injectedRestoreRace = true;
realMkdirSync(cacheDir, { recursive: true, mode: 0o700 });
fs.writeFileSync(concurrentMarkerPath, 'preserve concurrent cache\n');
throw Object.assign(new Error('simulated concurrent create'), { code: 'EEXIST' });
}
return realMkdirSync(...args);
}
);
try {
expect(() => ensureCodexProfileResources(profileDir, { sharedCodexHome })).toThrow(
'simulated filesystem corruption'
);
} finally {
mkdirSpy.mockRestore();
symlinkSpy.mockRestore();
}
expect(fs.readFileSync(concurrentMarkerPath, 'utf8')).toBe('preserve concurrent cache\n');
expect(fs.readFileSync(staleSkillPath, 'utf8')).toBe('# Stale profile skill\n');
expect(
fs.readdirSync(path.dirname(cacheDir)).some((name) => name.startsWith('.cache.ccs-backup-'))
).toBe(false);
});
it('preserves a concurrent non-directory cache path during fallback restoration', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
const pluginsDir = path.join(profileDir, 'plugins');
const cacheDir = path.join(pluginsDir, 'cache');
const staleSkillPath = path.join(cacheDir, staleCachedSkillRelativePath);
fs.mkdirSync(path.dirname(staleSkillPath), { recursive: true, mode: 0o700 });
fs.writeFileSync(staleSkillPath, '# Stale profile skill\n');
const realSymlinkSync = fs.symlinkSync.bind(fs);
const symlinkSpy = spyOn(fs, 'symlinkSync').mockImplementation(
(...args: Parameters<typeof fs.symlinkSync>) => {
const [, pathToCreate] = args;
if (path.resolve(String(pathToCreate)) === path.resolve(cacheDir)) {
throw Object.assign(new Error('simulated Windows symlink denial'), { code: 'EPERM' });
}
return realSymlinkSync(...args);
}
);
const realMkdirSync = fs.mkdirSync.bind(fs);
let injectedRestoreRace = false;
const mkdirSpy = spyOn(fs, 'mkdirSync').mockImplementation(
(...args: Parameters<typeof fs.mkdirSync>) => {
const [pathToCreate] = args;
if (!injectedRestoreRace && path.resolve(String(pathToCreate)) === path.resolve(cacheDir)) {
injectedRestoreRace = true;
fs.writeFileSync(cacheDir, 'preserve concurrent file\n');
throw Object.assign(new Error('simulated concurrent create'), { code: 'EEXIST' });
}
return realMkdirSync(...args);
}
);
try {
expect(() => ensureCodexProfileResources(profileDir, { sharedCodexHome })).toThrow(
'profile cache path is not a local directory'
);
} finally {
mkdirSpy.mockRestore();
symlinkSpy.mockRestore();
}
expect(fs.readFileSync(cacheDir, 'utf8')).toBe('preserve concurrent file\n');
const backupNames = fs
.readdirSync(pluginsDir)
.filter((name) => name.startsWith('.cache.ccs-backup-'));
expect(backupNames).toHaveLength(1);
expect(
fs.readFileSync(path.join(pluginsDir, backupNames[0], staleCachedSkillRelativePath), 'utf8')
).toBe('# Stale profile skill\n');
});
it('repairs a missing resource link without changing existing shared files', async () => {
@@ -107,4 +419,88 @@ describe('ensureCodexProfileResources', () => {
expect(fs.lstatSync(agentsPath).isDirectory()).toBe(true);
expect(fs.existsSync(path.join(agentsPath, 'brainstormer.toml'))).toBe(true);
});
it('preserves a local plugin cache and copies missing shared entries on EPERM', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
ensureCodexProfileResources(profileDir, { sharedCodexHome });
const cacheDir = path.join(profileDir, 'plugins', 'cache');
fs.rmSync(cacheDir, { recursive: true, force: true });
fs.mkdirSync(path.join(cacheDir, path.dirname(staleCachedSkillRelativePath)), {
recursive: true,
mode: 0o700,
});
fs.writeFileSync(path.join(cacheDir, 'local-only.txt'), 'keep me\n');
fs.writeFileSync(path.join(cacheDir, staleCachedSkillRelativePath), '# Existing stale skill\n');
const realSymlinkSync = fs.symlinkSync.bind(fs);
const symlinkSpy = spyOn(fs, 'symlinkSync').mockImplementation(
(...args: Parameters<typeof fs.symlinkSync>) => {
const [, pathToCreate] = args;
if (path.resolve(String(pathToCreate)) === path.resolve(cacheDir)) {
throw Object.assign(new Error('simulated Windows symlink denial'), { code: 'EPERM' });
}
return realSymlinkSync(...args);
}
);
const origWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = () => true;
try {
ensureCodexProfileResources(profileDir, { sharedCodexHome });
} finally {
process.stderr.write = origWrite;
symlinkSpy.mockRestore();
}
expect(fs.lstatSync(cacheDir).isDirectory()).toBe(true);
expect(fs.readFileSync(path.join(cacheDir, 'local-only.txt'), 'utf8')).toBe('keep me\n');
expect(fs.readFileSync(path.join(cacheDir, staleCachedSkillRelativePath), 'utf8')).toBe(
'# Existing stale skill\n'
);
expect(fs.readFileSync(path.join(cacheDir, cachedSkillRelativePath), 'utf8')).toBe(
'# Current shared skill\n'
);
});
it('restores a stale plugin cache and rethrows an unexpected symlink error', async () => {
const { ensureCodexProfileResources } = await import(
'../../../src/codex-auth/codex-profile-resources'
);
ensureCodexProfileResources(profileDir, { sharedCodexHome });
const pluginsDir = path.join(profileDir, 'plugins');
const cacheDir = path.join(pluginsDir, 'cache');
const siblingPath = path.join(pluginsDir, 'marketplaces.json');
fs.rmSync(cacheDir, { recursive: true, force: true });
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
fs.writeFileSync(path.join(cacheDir, 'local-only.txt'), 'restore me\n');
fs.writeFileSync(siblingPath, '{"preserve":true}\n');
const realSymlinkSync = fs.symlinkSync.bind(fs);
const symlinkSpy = spyOn(fs, 'symlinkSync').mockImplementation(
(...args: Parameters<typeof fs.symlinkSync>) => {
const [, pathToCreate] = args;
if (path.resolve(String(pathToCreate)) === path.resolve(cacheDir)) {
throw Object.assign(new Error('simulated filesystem corruption'), { code: 'EIO' });
}
return realSymlinkSync(...args);
}
);
try {
expect(() => ensureCodexProfileResources(profileDir, { sharedCodexHome })).toThrow(
'simulated filesystem corruption'
);
} finally {
symlinkSpy.mockRestore();
}
expect(fs.lstatSync(cacheDir).isDirectory()).toBe(true);
expect(fs.readFileSync(path.join(cacheDir, 'local-only.txt'), 'utf8')).toBe('restore me\n');
expect(fs.existsSync(path.join(cacheDir, cachedSkillRelativePath))).toBe(false);
expect(fs.readFileSync(siblingPath, 'utf8')).toBe('{"preserve":true}\n');
});
});