This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -3,6 +3,7 @@ import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { WorkspaceManager, type WorkspaceConfig } from '../src/workspace-manager.js';
import { MultiMindCache } from '../src/multi-mind-cache.js';
describe('WorkspaceManager', () => {
let tmpDir: string;
@@ -17,6 +18,22 @@ describe('WorkspaceManager', () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('workspace root', () => {
it('rejects a pre-existing workspaces junction that escapes the data directory', () => {
const workspacesDir = path.join(tmpDir, 'workspaces');
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-ws-root-outside-'));
fs.rmSync(workspacesDir, { recursive: true, force: true });
fs.symlinkSync(outsideDir, workspacesDir, process.platform === 'win32' ? 'junction' : 'dir');
try {
expect(() => new WorkspaceManager(tmpDir)).toThrow(/workspace root/i);
} finally {
fs.unlinkSync(workspacesDir);
fs.rmSync(outsideDir, { recursive: true, force: true });
}
});
});
describe('create', () => {
it('creates workspace with directory, config, mind file, and sessions dir', () => {
const ws = manager.create({ name: 'My Project', group: 'Work' });
@@ -50,6 +67,34 @@ describe('WorkspaceManager', () => {
it('returns empty array when no workspaces exist', () => {
expect(manager.list()).toEqual([]);
});
it('omits a workspace whose config is a hard link to an outside file', () => {
manager.create({ name: 'Linked Config', group: 'Work' });
const configPath = path.join(tmpDir, 'workspaces', 'linked-config', 'workspace.json');
const outsidePath = path.join(tmpDir, 'outside-workspace.json');
fs.writeFileSync(outsidePath, JSON.stringify({
id: 'linked-config',
name: 'OUTSIDE-SECRET',
group: 'Work',
created: new Date().toISOString(),
}));
fs.unlinkSync(configPath);
fs.linkSync(outsidePath, configPath);
expect(manager.get('linked-config')).toBeNull();
expect(manager.list().some((workspace) => workspace.id === 'linked-config')).toBe(false);
expect(fs.readFileSync(outsidePath, 'utf8')).toContain('OUTSIDE-SECRET');
});
it('omits a workspace whose config identity does not match its directory', () => {
manager.create({ name: 'Expected Config', group: 'Work' });
const configPath = path.join(tmpDir, 'workspaces', 'expected-config', 'workspace.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as WorkspaceConfig;
fs.writeFileSync(configPath, JSON.stringify({ ...config, id: 'different-config' }));
expect(manager.get('expected-config')).toBeNull();
expect(manager.list().some((workspace) => workspace.id === 'different-config')).toBe(false);
});
});
describe('listByGroup', () => {
@@ -158,6 +203,33 @@ describe('WorkspaceManager', () => {
const wsDir = path.join(tmpDir, 'workspaces', 'to-delete');
expect(fs.existsSync(wsDir)).toBe(false);
});
it.each(['', '.', '..', '../escape', 'nested/escape', 'nested\\escape', 'C:\\escape'])(
'rejects unsafe workspace id %j without deleting outside the workspace root',
(id) => {
const sentinel = path.join(tmpDir, 'sentinel.txt');
fs.writeFileSync(sentinel, 'preserve me');
expect(() => manager.delete(id)).toThrow(/invalid workspace id/i);
expect(fs.readFileSync(sentinel, 'utf8')).toBe('preserve me');
expect(fs.statSync(path.join(tmpDir, 'workspaces')).isDirectory()).toBe(true);
},
);
it('preserves an on-disk directory whose workspace config cannot be validated', () => {
manager.create({ name: 'Untrusted Delete', group: 'Work' });
const workspaceDir = path.join(tmpDir, 'workspaces', 'untrusted-delete');
const configPath = path.join(workspaceDir, 'workspace.json');
const outsidePath = path.join(tmpDir, 'outside-delete.json');
fs.writeFileSync(outsidePath, JSON.stringify({ id: 'untrusted-delete' }));
fs.unlinkSync(configPath);
fs.linkSync(outsidePath, configPath);
manager.delete('untrusted-delete');
expect(fs.statSync(workspaceDir).isDirectory()).toBe(true);
expect(fs.readFileSync(outsidePath, 'utf8')).toContain('untrusted-delete');
});
});
describe('getMindPath', () => {
@@ -167,6 +239,95 @@ describe('WorkspaceManager', () => {
const mindPath = manager.getMindPath('mind-test');
expect(mindPath).toBe(path.join(tmpDir, 'workspaces', 'mind-test', 'workspace.mind'));
});
it.each(['missing-workspace', '..', '../escape', 'C:\\escape']) (
'rejects invalid or missing workspace id %s before resolving a mind path',
(id) => {
expect(() => manager.getMindPath(id)).toThrow(/workspace/i);
},
);
it('rejects an on-disk workspace whose config identity does not match', () => {
manager.create({ name: 'Expected Workspace', group: 'Work' });
const configPath = path.join(tmpDir, 'workspaces', 'expected-workspace', 'workspace.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as WorkspaceConfig;
fs.writeFileSync(configPath, JSON.stringify({ ...config, id: 'different-workspace' }));
expect(() => manager.getMindPath('expected-workspace')).toThrow(/config id/i);
});
it('rejects a dangling mind junction before opening its target', () => {
manager.create({ name: 'Linked Mind', group: 'Work' });
const mindPath = path.join(tmpDir, 'workspaces', 'linked-mind', 'workspace.mind');
const missingTarget = path.join(tmpDir, 'missing-mind-target');
fs.unlinkSync(mindPath);
fs.symlinkSync(missingTarget, mindPath, process.platform === 'win32' ? 'junction' : 'dir');
expect(() => manager.getMindPath('linked-mind')).toThrow(/regular file/i);
expect(fs.existsSync(missingTarget)).toBe(false);
fs.unlinkSync(mindPath);
});
it('rejects a mind file with another hard-link', () => {
manager.create({ name: 'Hard Linked Mind', group: 'Work' });
const mindPath = path.join(tmpDir, 'workspaces', 'hard-linked-mind', 'workspace.mind');
const outsidePath = path.join(tmpDir, 'outside.mind');
fs.writeFileSync(outsidePath, 'outside sentinel');
fs.unlinkSync(mindPath);
fs.linkSync(outsidePath, mindPath);
expect(() => manager.getMindPath('hard-linked-mind')).toThrow(/regular file/i);
expect(fs.readFileSync(outsidePath, 'utf8')).toBe('outside sentinel');
});
it('allows a valid workspace to recreate a missing mind inside its directory', () => {
manager.create({ name: 'Missing Mind', group: 'Work' });
const mindPath = path.join(tmpDir, 'workspaces', 'missing-mind', 'workspace.mind');
fs.unlinkSync(mindPath);
expect(manager.getMindPath('missing-mind')).toBe(mindPath);
const cache = new MultiMindCache({
maxOpen: 2,
getMindPath: id => manager.getMindPath(id),
allowedRoot: path.join(tmpDir, 'workspaces'),
});
expect(cache.getOrOpen('missing-mind')).not.toBeNull();
expect(fs.statSync(mindPath).isFile()).toBe(true);
cache.closeAll();
});
it('contains resolver failures and rejects a post-resolution junction swap', () => {
const throwingCache = new MultiMindCache({
maxOpen: 2,
getMindPath: () => { throw new Error('unsafe workspace'); },
allowedRoot: path.join(tmpDir, 'workspaces'),
});
expect(throwingCache.getOrOpen('missing')).toBeNull();
manager.create({ name: 'Swap Target', group: 'Work' });
const workspaceDir = path.join(tmpDir, 'workspaces', 'swap-target');
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-ws-swap-'));
const outsideMind = path.join(outsideDir, 'workspace.mind');
fs.writeFileSync(outsideMind, 'outside sentinel');
const cache = new MultiMindCache({
maxOpen: 2,
allowedRoot: path.join(tmpDir, 'workspaces'),
getMindPath: id => {
const resolved = manager.getMindPath(id);
fs.rmSync(workspaceDir, { recursive: true, force: true });
fs.symlinkSync(outsideDir, workspaceDir, process.platform === 'win32' ? 'junction' : 'dir');
return resolved;
},
});
try {
expect(cache.getOrOpen('swap-target')).toBeNull();
expect(fs.readFileSync(outsideMind, 'utf8')).toBe('outside sentinel');
} finally {
fs.unlinkSync(workspaceDir);
fs.rmSync(outsideDir, { recursive: true, force: true });
}
});
});
describe('listGroups', () => {
@@ -294,6 +455,96 @@ describe('WorkspaceManager', () => {
// Reverse-ported from OSS hive-mind (oss-drift triage R4, 2026-06-11).
describe('ensure', () => {
it('rejects a pre-existing workspace junction without writing through it', () => {
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-ws-outside-'));
const outsideConfig = path.join(outsideDir, 'workspace.json');
const outsideMind = path.join(outsideDir, 'workspace.mind');
fs.writeFileSync(outsideConfig, 'outside config');
fs.writeFileSync(outsideMind, 'outside mind');
const linkPath = path.join(tmpDir, 'workspaces', 'escape');
fs.symlinkSync(outsideDir, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
try {
expect(() => manager.ensure('escape')).toThrow(/already exists|valid workspace/i);
expect(fs.readFileSync(outsideConfig, 'utf8')).toBe('outside config');
expect(fs.readFileSync(outsideMind, 'utf8')).toBe('outside mind');
expect(fs.existsSync(path.join(outsideDir, 'sessions'))).toBe(false);
} finally {
fs.unlinkSync(linkPath);
fs.rmSync(outsideDir, { recursive: true, force: true });
}
});
it.each([
['malformed', '{"id":'],
['mismatched', JSON.stringify({ id: 'other-workspace' })],
])('preserves an existing %s workspace when ensure cannot validate it', (_label, rawConfig) => {
const wsDir = path.join(tmpDir, 'workspaces', 'victim');
fs.mkdirSync(wsDir);
const configPath = path.join(wsDir, 'workspace.json');
const mindPath = path.join(wsDir, 'workspace.mind');
fs.writeFileSync(configPath, rawConfig);
fs.writeFileSync(mindPath, 'mind sentinel');
expect(() => manager.ensure('victim')).toThrow(/already exists|valid workspace/i);
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawConfig);
expect(fs.readFileSync(mindPath, 'utf8')).toBe('mind sentinel');
expect(fs.existsSync(path.join(wsDir, 'sessions'))).toBe(false);
});
it.each([false, true])(
'adopts an empty legacy directory (sessions subdirectory: %s)',
(withSessions) => {
const workspaceDir = path.join(tmpDir, 'workspaces', 'default');
const sessionsDir = path.join(workspaceDir, 'sessions');
fs.mkdirSync(withSessions ? sessionsDir : workspaceDir, { recursive: true });
const workspace = manager.ensure('default', { name: 'Legacy Default', group: 'Work' });
expect(workspace.id).toBe('default');
expect(fs.statSync(sessionsDir).isDirectory()).toBe(true);
expect(fs.existsSync(path.join(workspaceDir, 'workspace.mind'))).toBe(true);
expect(manager.get('default')).toEqual(workspace);
},
);
it('does not adopt the legacy empty-directory shape for another workspace id', () => {
const workspaceDir = path.join(tmpDir, 'workspaces', 'not-default');
fs.mkdirSync(path.join(workspaceDir, 'sessions'), { recursive: true });
expect(() => manager.ensure('not-default')).toThrow(/already exists|valid workspace/i);
expect(fs.readdirSync(workspaceDir)).toEqual(['sessions']);
});
it('rejects a legacy sessions junction and preserves its outside target', () => {
const workspaceDir = path.join(tmpDir, 'workspaces', 'default');
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-legacy-sessions-'));
fs.mkdirSync(workspaceDir);
fs.symlinkSync(outsideDir, path.join(workspaceDir, 'sessions'), process.platform === 'win32' ? 'junction' : 'dir');
try {
expect(() => manager.ensure('default')).toThrow(/already exists|valid workspace/i);
expect(fs.readdirSync(outsideDir)).toEqual([]);
} finally {
fs.unlinkSync(path.join(workspaceDir, 'sessions'));
fs.rmSync(outsideDir, { recursive: true, force: true });
}
});
it.each([
['a legacy session file', 'sessions', 'session.jsonl'],
['an unexpected sibling', '', 'unexpected.txt'],
])('rejects legacy adoption when the directory contains %s', (_label, childDir, fileName) => {
const wsDir = path.join(tmpDir, 'workspaces', 'default');
const parent = path.join(wsDir, childDir);
fs.mkdirSync(parent, { recursive: true });
const sentinel = path.join(parent, fileName);
fs.writeFileSync(sentinel, 'preserve me');
expect(() => manager.ensure('default')).toThrow(/already exists|valid workspace/i);
expect(fs.readFileSync(sentinel, 'utf8')).toBe('preserve me');
expect(fs.existsSync(path.join(wsDir, 'workspace.mind'))).toBe(false);
});
it('creates a workspace with the exact supplied id when missing', () => {
const ws = manager.ensure('cwd-derived-id');