Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ac5c3b0
fix(desktop): keep cached Library sessions from mkdir empty custom roots
PraiseZhu Sep 19, 2026
dd7f150
fix(desktop): do not mint unavailable Library identity from default root
PraiseZhu Sep 19, 2026
f255e0d
fix(desktop): fail closed when a custom Library parent vanishes
PraiseZhu Sep 20, 2026
f43ab2a
fix(desktop): reject replaced custom parents and revoke extraDir on d…
PraiseZhu Sep 20, 2026
e5df252
fix(desktop): do not recursive-mkdir custom Library parent after inspect
PraiseZhu Sep 20, 2026
08627b3
fix(desktop): bind custom Library mkdir to held parent identity
PraiseZhu Sep 20, 2026
35ced66
fix(desktop): create custom Library trees via held parent fd
PraiseZhu Sep 20, 2026
5e19e9e
fix(desktop): skip custom first-open path usage persist and sweep
PraiseZhu Sep 20, 2026
0f372af
fix(desktop): existing custom open, session teardown, usage ledger
PraiseZhu Sep 20, 2026
0cf02ab
fix(desktop): Windows existing custom open via FILE_OPEN ReadFile
PraiseZhu Sep 20, 2026
11cb431
fix(desktop): parse existing-open helper JSON as complete values
PraiseZhu Sep 20, 2026
8cfa9d6
fix(desktop): do not delete custom Library uuid tmp/stream on open
PraiseZhu Sep 20, 2026
cb55ea8
feat(desktop): Windows custom Library first-create via NtCreateFile
PraiseZhu Sep 20, 2026
132dc98
test(maker-core): time durable Subagent system-deny waitFor
PraiseZhu Sep 20, 2026
94f36c0
fix(desktop): distinguish Windows meta exists from reparse collision
PraiseZhu Sep 20, 2026
c449c9c
Merge origin/main into feat/mivo-library-root-recovery-20260920
PraiseZhu Sep 20, 2026
d7c68cd
Merge origin/main into feat/mivo-library-root-recovery-20260920
PraiseZhu Sep 20, 2026
e734b46
fix(desktop): recycle extra tmp hard links without deleting unique or…
PraiseZhu Sep 21, 2026
2d031da
Revert "fix(desktop): recycle extra tmp hard links without deleting u…
PraiseZhu Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
335 changes: 335 additions & 0 deletions apps/desktop/src/main/cindy-brain/__tests__/librarySlot.test.ts

Large diffs are not rendered by default.

292 changes: 291 additions & 1 deletion apps/desktop/src/main/cindy-brain/__tests__/libraryVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* os.tmpdir 临时目录(规则 23:生成物不落仓库工作区),零 Electron。
* symlink 用例带能力探针(Windows 无特权时跳过;POSIX CI 实跑)。
*/
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
Expand All @@ -19,6 +19,7 @@ import {
type LibraryFileIdentity,
type LibraryReadHandle,
} from '../libraryVault.js';
import { initCustomLibraryTree, openExistingCustomLibrary, parseExistingStdout, PROVABLE_STAGING_NAME } from '../libraryDirFd.js';

const sha256Of = (s: string): string => createHash('sha256').update(s).digest('hex');

Expand Down Expand Up @@ -123,6 +124,295 @@ describe('LibraryVault', () => {
expect(stat.isDirectory()).toBe(true);
});

it('custom 用户父目录消失: open 报 disk-missing 且不重建空库; keep 仍在 rename 走的目录', async () => {
const parent = path.join(tmpRoot, 'picked');
const custom = path.join(parent, 'mivo-canvas');
await fs.promises.mkdir(custom, { recursive: true });
await fs.promises.writeFile(path.join(custom, 'keep.txt'), 'keep-me');
const first = makeVault({ rootDir: () => custom, locationKind: 'custom' });
const opened = await first.open();
expect(opened).toMatchObject({ ok: true, state: 'ready' });
await fs.promises.rename(parent, `${parent}.parked`);
const second = makeVault({ rootDir: () => custom, locationKind: 'custom' });
const missing = await second.open();
expect(missing).toMatchObject({ ok: true, state: 'unavailable', reason: 'disk-missing' });
expect(fs.existsSync(parent)).toBe(false);
expect(fs.existsSync(custom)).toBe(false);
expect(fs.existsSync(path.join(`${parent}.parked`, 'mivo-canvas', 'keep.txt'))).toBe(true);
});

it('custom 最后一次 inspect 后、骨架 mkdir 前父目录被移走:不得 recursive 重建空库', async () => {
const parent = path.join(tmpRoot, 'picked-411');
const custom = path.join(parent, 'mivo-canvas');
await fs.promises.mkdir(custom, { recursive: true });
await fs.promises.writeFile(path.join(custom, 'keep.txt'), 'keep-me');
const parentStat = await fs.promises.lstat(parent);
const grant = {
realPathAtGrant: await fs.promises.realpath(parent),
identity: { dev: parentStat.dev, ino: parentStat.ino },
};
const first = makeVault({ rootDir: () => custom, locationKind: 'custom', customParentGrant: grant });
expect(await first.open()).toMatchObject({ ok: true, state: 'ready' });
const parked = `${parent}.parked`;
let injected = false;
const raced = makeVault({
rootDir: () => custom,
locationKind: 'custom',
customParentGrant: grant,
openExistingCustom: async (req) => {
if (!injected) {
injected = true;
if (fs.existsSync(parent)) await fs.promises.rename(parent, parked);
}
return openExistingCustomLibrary(req);
},
});
const missing = await raced.open();
expect(injected).toBe(true);
expect(missing).toMatchObject({ ok: true, state: 'unavailable', reason: 'disk-missing' });
expect(fs.existsSync(parent)).toBe(false);
expect(fs.existsSync(path.join(custom, '.cindy-library', 'meta.json'))).toBe(false);
expect(fs.existsSync(path.join(parked, 'mivo-canvas', 'keep.txt'))).toBe(true);
});

it('最后一次成功 inspect 后、mkdir(root) 前换成同路径新 inode:不得在替换目录创建/写 meta', async () => {
if (process.platform === 'win32') return;
const parent = path.join(tmpRoot, 'picked-379');
const custom = path.join(parent, 'mivo-canvas');
await fs.promises.mkdir(custom, { recursive: true });
await fs.promises.writeFile(path.join(custom, 'keep.txt'), 'keep-me');
const parentStat = await fs.promises.lstat(parent);
const grant = {
realPathAtGrant: await fs.promises.realpath(parent),
identity: { dev: parentStat.dev, ino: parentStat.ino },
};
const first = makeVault({ rootDir: () => custom, locationKind: 'custom', customParentGrant: grant });
expect(await first.open()).toMatchObject({ ok: true, state: 'ready' });
const parked = `${parent}.parked`;
let injected = false;
const raced = makeVault({
rootDir: () => custom,
locationKind: 'custom',
customParentGrant: grant,
openExistingCustom: async (req) => {
if (!injected) {
injected = true;
if (fs.existsSync(parent)) await fs.promises.rename(parent, parked);
await fs.promises.mkdir(parent);
}
return openExistingCustomLibrary(req);
},
});
const opened = await raced.open();
expect(injected).toBe(true);
expect(opened).toMatchObject({ ok: true, state: 'unavailable', reason: 'binding-moved' });
expect(fs.existsSync(custom)).toBe(false);
expect(fs.existsSync(path.join(custom, '.cindy-library', 'meta.json'))).toBe(false);
expect(fs.existsSync(path.join(parked, 'mivo-canvas', 'keep.txt'))).toBe(true);
});

it('D: initCustomLibraryTree 后 sweep readdir 换根不得写 replacement usage.json', async () => {
if (process.platform === 'win32') return;
const parent = path.join(tmpRoot, 'picked-D');
const custom = path.join(parent, 'mivo-canvas');
await fs.promises.mkdir(parent);
const parentStat = await fs.promises.lstat(parent);
const grant = {
realPathAtGrant: await fs.promises.realpath(parent),
identity: { dev: parentStat.dev, ino: parentStat.ino },
};
const parked = `${parent}.parked`;
let afterInit = false;
let swapped = false;
const origReaddir = fs.promises.readdir.bind(fs.promises);
const origRename = fs.promises.rename.bind(fs.promises);
const origMkdir = fs.promises.mkdir.bind(fs.promises);
const origWriteFile = fs.promises.writeFile.bind(fs.promises);
const readdirSpy = vi.spyOn(fs.promises, 'readdir').mockImplementation(async (target, options) => {
const dest = String(target);
if (afterInit && !swapped && dest.includes(`${path.sep}mivo-canvas${path.sep}.cindy-library${path.sep}tmp`)) {
swapped = true;
if (fs.existsSync(parent)) await origRename(parent, parked);
await origMkdir(parent);
await origMkdir(custom);
await origMkdir(path.join(custom, '.cindy-library'));
await origMkdir(path.join(custom, '.cindy-library', 'tmp'));
await origWriteFile(path.join(custom, '.cindy-library', 'meta.json'), JSON.stringify({
version: 1, ghostId: 'mivo-canvas', createdAt: 1,
}));
await origWriteFile(path.join(custom, 'user-keep.txt'), 'user');
await origWriteFile(path.join(custom, '.cindy-library', 'tmp', 'old.tmp'), 'stale');
}
return origReaddir(target, options);
});
const vault = makeVault({
rootDir: () => custom,
locationKind: 'custom',
customParentGrant: grant,
ghostId: 'mivo-canvas',
initCustomTree: async (req) => {
const r = await initCustomLibraryTree(req);
afterInit = true;
return r;
},
});
const opened = await vault.open();
readdirSpy.mockRestore();
expect(opened.ok).toBe(true);
expect(fs.existsSync(path.join(custom, '.cindy-library', 'usage.json'))).toBe(false);
if (swapped) {
expect(fs.existsSync(path.join(custom, 'user-keep.txt'))).toBe(true);
expect(opened).toMatchObject({ state: 'unavailable' });
} else {
expect(opened).toMatchObject({ state: 'ready' });
expect(fs.existsSync(path.join(parent, 'mivo-canvas', '.cindy-library', 'usage.json'))).toBe(false);
}
});

it('已有 custom 再 open 走 existing,不调用 create helper', async () => {
const parent = path.join(tmpRoot, 'picked-exist');
const custom = path.join(parent, 'mivo-canvas');
await fs.promises.mkdir(custom, { recursive: true });
const parentStat = await fs.promises.lstat(parent);
const grant = {
realPathAtGrant: await fs.promises.realpath(parent),
identity: { dev: parentStat.dev, ino: parentStat.ino },
};
const first = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
});
expect(await first.open()).toMatchObject({ ok: true, state: 'ready' });
const init = vi.fn(async () => ({ ok: false as const, code: 'UNSUPPORTED' as const }));
const second = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
initCustomTree: init,
});
expect(await second.open()).toMatchObject({ ok: true, state: 'ready' });
expect(init).not.toHaveBeenCalled();
});

it('existing UNSUPPORTED 且无完整结构: permission 且不 mkdir', async () => {
const parent = path.join(tmpRoot, 'picked-win');
await fs.promises.mkdir(parent);
const custom = path.join(parent, 'mivo-canvas');
const parentStat = await fs.promises.lstat(parent);
const grant = {
realPathAtGrant: await fs.promises.realpath(parent),
identity: { dev: parentStat.dev, ino: parentStat.ino },
};
const vault = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
openExistingCustom: async () => ({ ok: false as const, code: 'UNSUPPORTED' as const }),
initCustomTree: async () => ({ ok: false as const, code: 'UNSUPPORTED' as const }),
});
const opened = await vault.open();
expect(opened).toMatchObject({ ok: true, state: 'unavailable', reason: 'permission' });
expect(fs.existsSync(custom)).toBe(false);
});

it('合法 usage.json 只读复用,不因 custom open 丢账本', async () => {
const parent = path.join(tmpRoot, 'picked-ledger');
const custom = path.join(parent, 'mivo-canvas');
await fs.promises.mkdir(custom, { recursive: true });
const parentStat = await fs.promises.lstat(parent);
const grant = {
realPathAtGrant: await fs.promises.realpath(parent),
identity: { dev: parentStat.dev, ino: parentStat.ino },
};
const first = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
});
expect(await first.open()).toMatchObject({ ok: true, state: 'ready' });
const w = await first.write({ path: 'keep.txt', content: 'abcdef' });
expect(w.ok).toBe(true);
const second = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
});
const opened = await second.open();
expect(opened).toMatchObject({ ok: true, state: 'ready', usedBytes: Buffer.byteLength('abcdef') });
});

it('existing-open payload: pretty 与 compact 合法 meta 都读, malformed 仍 CORRUPT', () => {
const compact = 'OK\n{"version":1,"ghostId":"mivo-canvas","createdAt":1}\n{"files":2,"bytes":10,"updatedAt":1,"mutations":0}';
const prettyMeta = JSON.stringify({ version: 1, ghostId: 'mivo-canvas', createdAt: 1 }, null, 2);
const prettyUsage = JSON.stringify({ files: 2, bytes: 10, updatedAt: 1, mutations: 0 }, null, 2);
const pretty = `OK\n${prettyMeta}\n${prettyUsage}`;
expect(parseExistingStdout(compact)).toMatchObject({
ok: true, meta: { version: 1, ghostId: 'mivo-canvas', createdAt: 1 }, usage: { files: 2, bytes: 10 },
});
expect(parseExistingStdout(pretty)).toMatchObject({
ok: true, meta: { version: 1, ghostId: 'mivo-canvas', createdAt: 1 }, usage: { files: 2, bytes: 10 },
});
expect(parseExistingStdout('OK\n{"version":1,"ghostId":"mivo-canvas","createdAt":1}')).toMatchObject({
ok: true, usage: null,
});
expect(parseExistingStdout('OK\n{not json')).toMatchObject({ ok: false, code: 'CORRUPT' });
expect(parseExistingStdout('OK\n{"version":2,"ghostId":"mivo-canvas","createdAt":1}')).toMatchObject({ ok: false, code: 'CORRUPT' });
expect(parseExistingStdout('OK\n{"version":1,"ghostId":"mivo-canvas","createdAt":1}\n{nope')).toMatchObject({ ok: false, code: 'CORRUPT' });
expect(parseExistingStdout('MISSING')).toMatchObject({ ok: false, code: 'MISSING' });
});

it('pretty-printed 落盘 meta 再 open 仍 ready,不降校验', async () => {
const parent = path.join(tmpRoot, 'picked-pretty');
const custom = path.join(parent, 'mivo-canvas');
await fs.promises.mkdir(custom, { recursive: true });
const parentStat = await fs.promises.lstat(parent);
const grant = {
realPathAtGrant: await fs.promises.realpath(parent),
identity: { dev: parentStat.dev, ino: parentStat.ino },
};
const first = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
});
expect(await first.open()).toMatchObject({ ok: true, state: 'ready' });
const metaPath = path.join(custom, '.cindy-library', 'meta.json');
const compact = JSON.parse(await fs.promises.readFile(metaPath, 'utf8'));
await fs.promises.writeFile(metaPath, JSON.stringify(compact, null, 2));
const second = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
});
expect(await second.open()).toMatchObject({ ok: true, state: 'ready' });
await fs.promises.writeFile(metaPath, '{not json');
const third = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
});
expect(await third.open()).toMatchObject({ ok: true, state: 'unavailable', reason: 'corrupt' });
});

it('可证 staging 名只匹配 uuid.tmp/stream,不匹配 old.tmp 或原件', () => {
expect(PROVABLE_STAGING_NAME.test('25ae5922-06f7-46dd-99f1-6d914d53af33.tmp')).toBe(true);
expect(PROVABLE_STAGING_NAME.test('25ae5922-06f7-46dd-99f1-6d914d53af33.stream')).toBe(true);
expect(PROVABLE_STAGING_NAME.test('old.tmp')).toBe(false);
expect(PROVABLE_STAGING_NAME.test('keep.txt')).toBe(false);
expect(PROVABLE_STAGING_NAME.test('meta.json')).toBe(false);
});

it('Windows 新建 custom 走稳定 parent handle 首建 ready', async () => {
if (process.platform !== 'win32') return;
const parent = path.join(tmpRoot, 'picked-win-new');
await fs.promises.mkdir(parent);
const custom = path.join(parent, 'mivo-canvas');
const parentStat = await fs.promises.lstat(parent);
const grant = {
realPathAtGrant: await fs.promises.realpath(parent),
identity: { dev: parentStat.dev, ino: parentStat.ino },
};
const vault = makeVault({
rootDir: () => custom, locationKind: 'custom', customParentGrant: grant, ghostId: 'mivo-canvas',
});
const opened = await vault.open();
expect(opened).toMatchObject({ ok: true, state: 'ready' });
expect(fs.existsSync(path.join(custom, '.cindy-library', 'meta.json'))).toBe(true);
});

it('default 缺失根仍可首次创建', async () => {
const missing = path.join(tmpRoot, 'brand-new-default', 'ghost');
const vault = makeVault({ rootDir: () => missing, locationKind: 'default' });
const opened = await vault.open();
expect(opened).toMatchObject({ ok: true, state: 'ready' });
expect(fs.existsSync(path.join(missing, '.cindy-library', 'meta.json'))).toBe(true);
});

it('meta 损坏 → unavailable(corrupt),绝不静默重建空库', async () => {
await fs.promises.mkdir(path.join(libraryRoot, '.cindy-library'), { recursive: true });
await fs.promises.writeFile(path.join(libraryRoot, '.cindy-library', 'meta.json'), '{not json');
Expand Down
Loading
Loading