mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 15:22:59 +00:00
0b50c88eb3
POST /api/fleet/nodes/:id/update and POST /api/fleet/update-all read node.api_url + node.api_token directly, so a pilot-agent row (which carries neither) returned "Remote node not configured." and was filtered out of bulk update. Both routes now resolve the target via NodeRegistry.getProxyTarget(), which returns the loopback URL for an active pilot tunnel and the configured api_url for proxy-mode remotes. fetchMetaForNode replaces the direct fetchRemoteMeta call for the self-update capability check. self-update is removed from PILOT_DISABLED_CAPABILITIES so a Compose-deployed pilot can advertise it; the host-console filter stays. SelfUpdateService still gates the local-end capability on the container actually carrying docker-compose labels, so a docker-run pilot self-disables and the Fleet UI sees a clean 503 instead of an ambiguous failure. Tests: new fleet-pilot-update covers the four single-node branches (success via loopback, null target, no self-update capability, meta offline) and two update-all cases (mixed-fleet dispatch, all-targets- null skip). capability-registry-pilot rewired to assert the new filter set.
101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
/**
|
|
* F9 regression guard for CapabilityRegistry:
|
|
*
|
|
* - fetchRemoteMeta omits the Authorization header when the apiToken is
|
|
* empty so the loopback bridge (used by pilot-agent proxy targets) is
|
|
* not handed a malformed `Bearer ` header.
|
|
* - applyPilotModeCapabilityFilter strips host-console (whose central->pilot
|
|
* WS upgrade path is not yet wired) but leaves self-update in place so a
|
|
* Compose-deployed pilot can advertise it and the Fleet Update flow can
|
|
* route through NodeRegistry.getProxyTarget().
|
|
*/
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import axios from 'axios';
|
|
|
|
import {
|
|
CAPABILITIES,
|
|
applyPilotModeCapabilityFilter,
|
|
enableCapability,
|
|
fetchRemoteMeta,
|
|
getActiveCapabilities,
|
|
} from '../services/CapabilityRegistry';
|
|
|
|
describe('fetchRemoteMeta Authorization header', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('sends Authorization: Bearer <token> when token is non-empty', async () => {
|
|
const getSpy = vi.spyOn(axios, 'get').mockResolvedValue({
|
|
data: { version: '0.76.7', capabilities: ['stacks'], startedAt: 1, updateError: null },
|
|
});
|
|
|
|
await fetchRemoteMeta('https://remote.example.com:1852', 'real-token');
|
|
|
|
expect(getSpy).toHaveBeenCalledTimes(1);
|
|
const init = getSpy.mock.calls[0][1] as { headers: Record<string, string> };
|
|
expect(init.headers).toEqual({ Authorization: 'Bearer real-token' });
|
|
});
|
|
|
|
it('omits Authorization entirely when token is empty (pilot-agent loopback)', async () => {
|
|
const getSpy = vi.spyOn(axios, 'get').mockResolvedValue({
|
|
data: { version: '0.76.7', capabilities: ['stacks'], startedAt: 1, updateError: null },
|
|
});
|
|
|
|
await fetchRemoteMeta('http://127.0.0.1:54321', '');
|
|
|
|
expect(getSpy).toHaveBeenCalledTimes(1);
|
|
const init = getSpy.mock.calls[0][1] as { headers: Record<string, string> };
|
|
expect(init.headers).toEqual({});
|
|
expect(init.headers).not.toHaveProperty('Authorization');
|
|
});
|
|
|
|
it('returns OFFLINE_META shape on transport failure', async () => {
|
|
vi.spyOn(axios, 'get').mockRejectedValue(new Error('connect ECONNREFUSED'));
|
|
|
|
const meta = await fetchRemoteMeta('http://127.0.0.1:54321', '');
|
|
|
|
expect(meta).toEqual({
|
|
version: null,
|
|
capabilities: [],
|
|
startedAt: null,
|
|
updateError: null,
|
|
online: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('applyPilotModeCapabilityFilter', () => {
|
|
afterEach(() => {
|
|
enableCapability('host-console');
|
|
});
|
|
|
|
it('removes host-console from active capabilities', () => {
|
|
expect(CAPABILITIES).toContain('host-console');
|
|
|
|
applyPilotModeCapabilityFilter();
|
|
const active = getActiveCapabilities();
|
|
|
|
expect(active).not.toContain('host-console');
|
|
expect(active).toContain('stacks');
|
|
});
|
|
|
|
it('leaves self-update in place so Compose-deployed pilots can advertise it', () => {
|
|
expect(CAPABILITIES).toContain('self-update');
|
|
|
|
applyPilotModeCapabilityFilter();
|
|
const active = getActiveCapabilities();
|
|
|
|
expect(active).toContain('self-update');
|
|
});
|
|
|
|
it('is idempotent (safe to call multiple times)', () => {
|
|
applyPilotModeCapabilityFilter();
|
|
applyPilotModeCapabilityFilter();
|
|
const active = getActiveCapabilities();
|
|
|
|
expect(active).not.toContain('host-console');
|
|
expect(active.length).toBe(CAPABILITIES.length - 1);
|
|
});
|
|
});
|