Files
NetMesh/domain/vaultGroupAgentOps.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

261 lines
12 KiB
TypeScript

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import type { Host, ManagedSource, ProxyProfile } from './models';
import { deleteGroup, upsertGroup } from './vaultGroupAgentOps';
const hosts: Host[] = [
{ id: 'host-1', label: 'Prod', hostname: 'prod.test', username: 'root', group: 'prod/web', tags: [], os: 'linux' },
{ id: 'jump-1', label: 'Jump', hostname: 'jump.test', username: 'root', tags: [], os: 'linux' },
];
const proxyProfiles: ProxyProfile[] = [{
id: 'proxy-1', label: 'Proxy', config: { type: 'socks5', host: '127.0.0.1', port: 1080 }, createdAt: 1,
}];
describe('vaultGroupAgentOps', () => {
it('renames a group and descendants across configs, hosts, and managed sources', () => {
const result = upsertGroup({
groups: ['prod', 'prod/web'], configs: [{ path: 'prod/web', username: 'old' }], hosts,
managedSources: [{ id: 'source-1', groupName: 'prod/web' } as ManagedSource],
}, 'prod', '{"username":"deploy","proxyProfileId":"proxy-1","jumpHostIds":["jump-1"]}', [], proxyProfiles, { newPath: 'production' });
assert.equal(result.ok, true);
if (!result.ok) return;
assert.deepEqual(result.state.groups, ['production', 'production/web']);
assert.equal(result.state.hosts[0]?.group, 'production/web');
assert.equal(result.state.managedSources[0]?.groupName, 'production/web');
assert.equal(result.config?.username, 'deploy');
});
it('rejects missing identities, proxy profiles, and jump hosts', () => {
const state = { groups: ['prod'], configs: [], hosts, managedSources: [] };
assert.equal(upsertGroup(state, 'prod', '{"identityId":"missing"}', [], proxyProfiles).ok, false);
assert.equal(upsertGroup(state, 'prod', '{"proxyProfileId":"missing"}', [], proxyProfiles).ok, false);
assert.equal(upsertGroup(state, 'prod', '{"jumpHostIds":["missing"]}', [], proxyProfiles).ok, false);
assert.equal(upsertGroup(state, 'prod', '{"moshEnabled":true,"etEnabled":true}', [], proxyProfiles).ok, false);
});
it('rejects jump hosts that do not resolve to SSH', () => {
const localHost: Host = {
id: 'local', label: 'Local', hostname: 'localhost', username: '', protocol: 'local', tags: [], os: 'linux',
};
const inheritedHost: Host = {
id: 'inherited', label: 'Inherited', hostname: 'inherited.test', username: 'root', group: 'telnet/child', tags: [], os: 'linux',
};
const state = {
groups: ['prod', 'telnet', 'telnet/child'],
configs: [{ path: 'telnet', protocol: 'telnet' as const }],
hosts: [...hosts, localHost, inheritedHost],
managedSources: [],
};
const localResult = upsertGroup(state, 'prod', '{"jumpHostIds":["local"]}', [], proxyProfiles);
const inheritedResult = upsertGroup(state, 'prod', '{"jumpHostIds":["inherited"]}', [], proxyProfiles);
assert.equal(localResult.ok, false);
assert.equal(inheritedResult.ok, false);
});
it('rejects Mosh and ET defaults that conflict with inherited group settings', () => {
const inheritedEt = upsertGroup({
groups: ['prod', 'prod/child'],
configs: [{ path: 'prod', etEnabled: true }],
hosts: [],
managedSources: [],
}, 'prod/child', { moshEnabled: true }, [], proxyProfiles);
assert.equal(inheritedEt.ok, false);
const inheritedTelnet = upsertGroup({
groups: ['prod', 'prod/child'],
configs: [{ path: 'prod', protocol: 'telnet' }],
hosts: [],
managedSources: [],
}, 'prod/child', { moshEnabled: true }, [], proxyProfiles);
assert.equal(inheritedTelnet.ok, false);
});
it('allows valid group edits when unrelated legacy transport settings are invalid', () => {
const result = upsertGroup({
groups: ['legacy', 'valid'],
configs: [{ path: 'legacy', protocol: 'telnet', moshEnabled: true }],
hosts: [{
id: 'legacy-host', label: 'legacy', hostname: 'legacy.test', username: 'root',
group: 'legacy', protocol: 'telnet', moshEnabled: true, tags: [], os: 'linux',
}],
managedSources: [],
}, 'valid', { username: 'deploy' }, [], proxyProfiles);
assert.equal(result.ok, true);
if (result.ok) assert.equal(result.config?.username, 'deploy');
});
it('validates jump hosts against their destination group after a rename', () => {
const jump: Host = {
id: 'moving-jump', label: 'Moving jump', hostname: 'jump.test', username: 'root',
group: 'ssh/prod', hostChain: { hostIds: [] }, tags: [], os: 'linux',
};
const toTelnet = upsertGroup({
groups: ['ssh', 'ssh/prod', 'telnet'],
configs: [{ path: 'ssh', protocol: 'ssh' }, { path: 'telnet', protocol: 'telnet' }],
hosts: [jump],
managedSources: [],
}, 'ssh/prod', '{"jumpHostIds":["moving-jump"]}', [], proxyProfiles, { newPath: 'telnet/prod' });
const toSsh = upsertGroup({
groups: ['ssh', 'telnet', 'telnet/prod'],
configs: [{ path: 'ssh', protocol: 'ssh' }, { path: 'telnet', protocol: 'telnet' }],
hosts: [{ ...jump, group: 'telnet/prod' }],
managedSources: [],
}, 'telnet/prod', '{"jumpHostIds":["moving-jump"]}', [], proxyProfiles, { newPath: 'ssh/prod' });
assert.equal(toTelnet.ok, false);
assert.equal(toSsh.ok, true);
});
it('rejects group changes that introduce invalid inherited jump relationships', () => {
const jump: Host = {
id: 'moving-jump', label: 'Moving jump', hostname: 'jump.test', username: 'root',
group: 'ssh/prod', tags: [], os: 'linux',
};
const outsideTarget: Host = {
id: 'outside-target', label: 'Outside target', hostname: 'target.test', username: 'root',
hostChain: { hostIds: [jump.id] }, tags: [], os: 'linux',
};
const renameResult = upsertGroup({
groups: ['ssh', 'ssh/prod', 'telnet'],
configs: [{ path: 'ssh', protocol: 'ssh' }, { path: 'telnet', protocol: 'telnet' }],
hosts: [jump, outsideTarget],
managedSources: [],
}, 'ssh/prod', '{}', [], proxyProfiles, { newPath: 'telnet/prod' });
const selfResult = upsertGroup({
groups: ['prod'], configs: [], hosts: [{ ...jump, group: 'prod' }], managedSources: [],
}, 'prod', '{"jumpHostIds":["moving-jump"]}', [], proxyProfiles);
assert.equal(renameResult.ok, false);
assert.equal(selfResult.ok, false);
});
it('keeps jump hosts referenced by empty group defaults SSH-capable after a rename', () => {
const jump: Host = {
id: 'jump', label: 'Jump', hostname: 'jump.test', username: 'root',
group: 'ssh/prod', tags: [], os: 'linux',
};
const result = upsertGroup({
groups: ['ssh', 'ssh/prod', 'telnet', 'empty'],
configs: [
{ path: 'ssh', protocol: 'ssh' },
{ path: 'telnet', protocol: 'telnet' },
{ path: 'empty', hostChain: { hostIds: [jump.id] } },
],
hosts: [jump],
managedSources: [],
}, 'ssh/prod', '{}', [], proxyProfiles, { newPath: 'telnet/prod' });
assert.equal(result.ok, false);
if (!result.ok) assert.match(result.error, /must keep an SSH connection type/i);
});
it('keeps create distinct from update and rejects self-descendant moves', () => {
const state = { groups: ['prod', 'prod/web'], configs: [{ path: 'prod' }], hosts, managedSources: [] };
assert.equal(upsertGroup(state, 'prod', '{}', [], proxyProfiles, { create: true }).ok, false);
assert.equal(upsertGroup(state, 'prod', '{}', [], proxyProfiles, { newPath: 'prod/archive' }).ok, false);
});
it('rejects renames whose descendants collide with existing groups or configs', () => {
const groupCollision = upsertGroup({
groups: ['prod', 'prod/web', 'archive/web'], configs: [], hosts, managedSources: [],
}, 'prod', '{}', [], proxyProfiles, { newPath: 'archive' });
assert.equal(groupCollision.ok, false);
const configCollision = upsertGroup({
groups: ['prod', 'prod/web'], configs: [{ path: 'archive/web' }], hosts, managedSources: [],
}, 'prod', '{}', [], proxyProfiles, { newPath: 'archive' });
assert.equal(configCollision.ok, false);
});
it('clears identity-derived defaults when replacing or detaching an identity', () => {
const state = {
groups: ['prod'],
configs: [{ path: 'prod', identityId: 'old', username: 'old-user', authMethod: 'password' as const, savePassword: true }],
hosts,
managedSources: [],
};
const identity = { id: 'new', label: 'New', username: 'deploy', authMethod: 'key' as const, keyId: 'key-1', created: 1 };
const replaced = upsertGroup(state, 'prod', '{"identityId":"new"}', [identity], proxyProfiles);
assert.equal(replaced.ok, true);
if (replaced.ok) {
assert.equal(replaced.config?.username, 'deploy');
assert.equal(replaced.config?.savePassword, undefined);
}
const detached = upsertGroup(state, 'prod', '{"identityId":""}', [identity], proxyProfiles);
assert.equal(detached.ok, true);
if (detached.ok) {
assert.equal(detached.config?.identityId, '');
assert.equal(detached.config?.username, undefined);
assert.equal(detached.config?.authMethod, undefined);
}
const manualUsername = upsertGroup(state, 'prod', '{"username":"ops"}', [identity], proxyProfiles);
assert.equal(manualUsername.ok, true);
if (manualUsername.ok) {
assert.equal(manualUsername.config?.username, 'ops');
assert.equal(manualUsername.config?.identityId, '');
assert.equal(manualUsername.config?.authMethod, undefined);
}
const detachedWithUsername = upsertGroup(
state,
'prod',
'{"identityId":"","username":"ops"}',
[identity],
proxyProfiles,
);
assert.equal(detachedWithUsername.ok, true);
if (detachedWithUsername.ok) {
assert.equal(detachedWithUsername.config?.identityId, '');
assert.equal(detachedWithUsername.config?.username, 'ops');
}
});
it('moves hosts to root by default and refuses managed group deletion', () => {
const state = { groups: ['prod', 'prod/web'], configs: [{ path: 'prod' }], hosts, managedSources: [] };
const removed = deleteGroup(state, 'prod', false);
assert.equal(removed.ok, true);
if (removed.ok) assert.equal(removed.state.hosts[0]?.group, undefined);
const managed = deleteGroup({
...state, managedSources: [{ id: 'source-1', groupName: 'prod' } as ManagedSource],
}, 'prod', false);
assert.equal(managed.ok, false);
});
it('refuses to delete hosts that are still used as jump hosts', () => {
const jump = { ...hosts[1]!, group: 'prod' };
const target: Host = {
id: 'target', label: 'Target', hostname: 'target.test', username: 'root',
hostChain: { hostIds: [jump.id] }, tags: [], os: 'linux',
};
const result = deleteGroup({
groups: ['prod'], configs: [], hosts: [jump, target], managedSources: [],
}, 'prod', true);
assert.equal(result.ok, false);
});
it('refuses to delete hosts referenced by empty group defaults outside the deleted group', () => {
const jump = { ...hosts[1]!, group: 'temporary' };
const result = deleteGroup({
groups: ['temporary', 'empty'],
configs: [{ path: 'empty', hostChain: { hostIds: [jump.id] } }],
hosts: [jump],
managedSources: [],
}, 'temporary', true);
assert.equal(result.ok, false);
});
it('preserves managed status when deleting a subgroup under a managed parent', () => {
const managedHosts = hosts.map((host) => host.group === 'prod/web' ? { ...host, managedSourceId: 'source-1' } : host);
const result = deleteGroup({
groups: ['prod', 'prod/web'], configs: [], hosts: managedHosts,
managedSources: [{ id: 'source-1', groupName: 'prod' } as ManagedSource],
}, 'prod/web', false);
assert.equal(result.ok, true);
if (result.ok) assert.equal(result.state.hosts[0]?.managedSourceId, 'source-1');
});
});