mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Align downloaded install handoff copy
This commit is contained in:
@@ -171,6 +171,9 @@ management, and fleet control surfaces.
|
||||
path must also auto-create the scoped first-host install token so the
|
||||
operator lands on ready-to-copy commands instead of being asked to perform a
|
||||
second manual token-generation step immediately after securing the server.
|
||||
Any first-run credentials download generated from that same handoff must
|
||||
describe the prepared first-host token path consistently instead of telling
|
||||
the operator to generate another install token manually.
|
||||
9. Keep `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
|
||||
ordered around the actual first-run operator sequence: credentials that must
|
||||
be saved now should be visible before the operator leaves the screen, and
|
||||
|
||||
@@ -223,6 +223,10 @@ Own canonical runtime payload shapes between backend and frontend.
|
||||
token-creation contract must remain usable immediately from the install
|
||||
workspace so the first-host flow can auto-create the scoped install token
|
||||
without forcing the operator through a second manual token-generation step.
|
||||
Any downloaded first-run handoff instructions emitted by that same shared
|
||||
install-state surface must describe that prepared token path consistently
|
||||
with the live runtime behavior rather than directing the operator to create
|
||||
another install token manually.
|
||||
|
||||
## Current State
|
||||
|
||||
|
||||
+77
@@ -26,6 +26,23 @@ import type {
|
||||
} from '@/types/api';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
|
||||
const createObjectURLMock = vi.fn(() => 'blob:mock-url');
|
||||
const revokeObjectURLMock = vi.fn();
|
||||
|
||||
class MockBlob {
|
||||
readonly parts: string[];
|
||||
readonly type: string;
|
||||
|
||||
constructor(parts: unknown[], options?: { type?: string }) {
|
||||
this.parts = parts.map((part) => String(part));
|
||||
this.type = options?.type || '';
|
||||
}
|
||||
|
||||
async text() {
|
||||
return this.parts.join('');
|
||||
}
|
||||
}
|
||||
|
||||
const { navigateMock } = vi.hoisted(() => ({
|
||||
navigateMock: vi.fn(),
|
||||
}));
|
||||
@@ -656,8 +673,15 @@ beforeEach(() => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
createObjectURLMock.mockReset().mockReturnValue('blob:mock-url');
|
||||
revokeObjectURLMock.mockReset();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('navigator', { clipboard: { writeText: clipboardSpy } } as unknown as Navigator);
|
||||
vi.stubGlobal('Blob', MockBlob as unknown as typeof Blob);
|
||||
vi.stubGlobal('URL', Object.assign(URL, {
|
||||
createObjectURL: createObjectURLMock,
|
||||
revokeObjectURL: revokeObjectURLMock,
|
||||
}));
|
||||
|
||||
listProfilesMock.mockResolvedValue([]);
|
||||
listAssignmentsMock.mockResolvedValue([]);
|
||||
@@ -721,6 +745,59 @@ describe('InfrastructureOperationsController token generation', () => {
|
||||
expect(screen.getByText('Installation commands')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('downloads first-run credentials that describe the prepared install token handoff', async () => {
|
||||
createTokenMock.mockResolvedValue({
|
||||
token: 'handoff-install-token',
|
||||
record: {
|
||||
id: 'token-record-handoff',
|
||||
name: 'Agent 2026-03-27 11-02',
|
||||
prefix: 'agent',
|
||||
suffix: '1234',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
const anchorClickMock = vi.fn();
|
||||
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
|
||||
const element = document.createElementNS('http://www.w3.org/1999/xhtml', tagName);
|
||||
if (tagName.toLowerCase() === 'a') {
|
||||
Object.defineProperty(element, 'click', {
|
||||
value: anchorClickMock,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
return element as HTMLElement;
|
||||
});
|
||||
|
||||
sessionStorage.setItem(
|
||||
STORAGE_KEYS.SETUP_HANDOFF,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'generated-password',
|
||||
apiToken: 'admin-token',
|
||||
createdAt: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
setupComponent();
|
||||
|
||||
await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Download credentials' }));
|
||||
|
||||
await waitFor(() => expect(createObjectURLMock).toHaveBeenCalled(), { interval: 0 });
|
||||
|
||||
const downloadedBlob = createObjectURLMock.mock.calls.at(-1)?.[0] as MockBlob | undefined;
|
||||
expect(downloadedBlob).toBeDefined();
|
||||
const content = await downloadedBlob!.text();
|
||||
expect(content).toContain('Canonical install workspace:');
|
||||
expect(content).toContain('/settings/infrastructure/install');
|
||||
expect(content).toContain(
|
||||
'Pulse prepares the first-host install token from setup so you can move straight to the generated Unified Agent install commands.',
|
||||
);
|
||||
|
||||
createElementSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('generates a token and shows confirmation', async () => {
|
||||
createTokenMock.mockResolvedValue({
|
||||
token: 'test-token-123',
|
||||
|
||||
@@ -195,7 +195,7 @@ Canonical install workspace:
|
||||
----------------------------
|
||||
${baseUrl.replace(/\/$/, '')}/settings/infrastructure/install
|
||||
|
||||
Generate a scoped install token below before copying Unified Agent install commands.
|
||||
Pulse prepares the first-host install token from setup so you can move straight to the generated Unified Agent install commands.
|
||||
`;
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
|
||||
Reference in New Issue
Block a user