Align downloaded install handoff copy

This commit is contained in:
rcourtman
2026-03-27 11:13:31 +00:00
parent ad9a46ee50
commit 05b94df130
4 changed files with 85 additions and 1 deletions
@@ -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
@@ -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' });