) => {
+ mocks.startSetup.mockResolvedValueOnce(finished(outcome));
+ const view = render();
+ fireEvent.click(screen.getByRole('button', { name: 'Start' }));
+ const step = (await screen.findByText(setupCopy.stages.installing)).closest('li')!;
+ await waitFor(() => expect(step).toHaveAttribute('data-state', 'done'));
+ const said = step.textContent;
+ view.unmount();
+ return said;
+ };
+
+ it('says it replaced a same-version copy, or updated an older one, as the command line does', async () => {
+ const replaced = await installStep(sameVersion);
+ expect(replaced).toContain(setupCopy.replaced('1.93.0'));
+ expect(replaced).not.toContain(setupCopy.installed('1.93.0'));
+ expect(
+ await installStep({
+ ...sameVersion,
+ upgraded_from: { ...sameVersion.upgraded_from, version: '1.92.1' },
+ })
+ ).toContain('Updated Crew from 1.92.1 to 1.93.0.');
+ expect(await installStep({ previous_kept: true, upgraded_from: null })).toContain(
+ setupCopy.replacedProgram('1.93.0')
+ );
+ });
+
+ it('still says installed for a fresh install, and already installed for a reuse', async () => {
+ expect(await installStep({ previous_kept: false, upgraded_from: null })).toContain(
+ 'Installed Crew 1.93.0.'
+ );
+ expect(await installStep({ reused: true })).toContain('Crew 1.93.0 was already installed.');
+ });
+});
diff --git a/ui/desktop/src/components/crew/onboarding/SetupProgress.tsx b/ui/desktop/src/components/crew/onboarding/SetupProgress.tsx
index 15d068b99..5910b926f 100644
--- a/ui/desktop/src/components/crew/onboarding/SetupProgress.tsx
+++ b/ui/desktop/src/components/crew/onboarding/SetupProgress.tsx
@@ -336,9 +336,15 @@ export function SetupProgress({
const installedDetail =
outcome?.installedVersion && outcome.reused
? setupCopy.reused(outcome.installedVersion)
- : outcome?.installedVersion
- ? setupCopy.installed(outcome.installedVersion)
- : null;
+ : outcome?.installedVersion && outcome.replacedVersion
+ ? outcome.replacedVersion === outcome.installedVersion
+ ? setupCopy.replaced(outcome.installedVersion)
+ : setupCopy.updated(outcome.replacedVersion, outcome.installedVersion)
+ : outcome?.installedVersion && outcome.previousKept
+ ? setupCopy.replacedProgram(outcome.installedVersion)
+ : outcome?.installedVersion
+ ? setupCopy.installed(outcome.installedVersion)
+ : null;
const jobId = job.run?.jobId ?? null;
return (
diff --git a/ui/desktop/src/components/crew/onboarding/copy.ts b/ui/desktop/src/components/crew/onboarding/copy.ts
index 522378059..effed34c6 100644
--- a/ui/desktop/src/components/crew/onboarding/copy.ts
+++ b/ui/desktop/src/components/crew/onboarding/copy.ts
@@ -647,6 +647,10 @@ export const setupCopy = {
/** Beside "Installing Crew in your account" once done. */
reused: (version: string) => `Crew ${version} was already installed.`,
installed: (version: string) => `Installed Crew ${version}.`,
+ /** An install that replaced a program (`setup_summary_lines` in the CLI says the same). */
+ updated: (from: string, to: string) => `Updated Crew from ${from} to ${to}.`,
+ replaced: (version: string) => `Replaced Crew ${version} with this computer’s copy of it.`,
+ replacedProgram: (version: string) => `Replaced the Crew program with Crew ${version}.`,
stop: 'Stop',
/** A Sign in window the person closed before signing in ends the job. */
signInEnded: 'Sign-in didn’t finish, so setup stopped. Try again when you’re ready to sign in.',
diff --git a/ui/desktop/src/components/crew/onboarding/setup.test.ts b/ui/desktop/src/components/crew/onboarding/setup.test.ts
index 422a31988..5d256f774 100644
--- a/ui/desktop/src/components/crew/onboarding/setup.test.ts
+++ b/ui/desktop/src/components/crew/onboarding/setup.test.ts
@@ -234,6 +234,8 @@ describe('setupRunFrom', () => {
expect(run.outcome).toEqual({
installedVersion: '1.94.0',
reused: true,
+ previousKept: false,
+ replacedVersion: null,
host: { kind: 'found', text: 'brcrew1:abc' },
hostCommand: 'umask 077',
connectionId: null,
@@ -245,6 +247,37 @@ describe('setupRunFrom', () => {
expect(plainStage(run)).toBe('ready');
});
+ it('reads which program an install replaced, and only a version that can be shown', () => {
+ const replacing = (upgraded_from: unknown) =>
+ setupRunFrom({
+ ...RUNNING,
+ state: 'finished',
+ stage: 'done',
+ outcome: {
+ installed_version: '1.93.0',
+ reused: false,
+ previous_kept: true,
+ upgraded_from,
+ },
+ }).outcome;
+ const sameVersion = {
+ version: '1.93.0',
+ sha256: 'fe'.repeat(32),
+ installed_version: '1.93.0',
+ installed_sha256: 'f4'.repeat(32),
+ };
+ expect(replacing(sameVersion)).toMatchObject({ previousKept: true, replacedVersion: '1.93.0' });
+ expect(replacing({ ...sameVersion, version: '1.92.1' })?.replacedVersion).toBe('1.92.1');
+ for (const bad of [
+ null,
+ '1.92.1',
+ { ...sameVersion, version: null },
+ { version: 'not-a-sha' },
+ ]) {
+ expect(replacing(bad)).toMatchObject({ previousKept: true, replacedVersion: null });
+ }
+ });
+
it('reads a failed job’s typed code, sentence and output', () => {
const run = setupRunFrom({
...RUNNING,
diff --git a/ui/desktop/src/components/crew/onboarding/setup.ts b/ui/desktop/src/components/crew/onboarding/setup.ts
index e41a7dcdf..7aea4449f 100644
--- a/ui/desktop/src/components/crew/onboarding/setup.ts
+++ b/ui/desktop/src/components/crew/onboarding/setup.ts
@@ -84,6 +84,10 @@ export interface SetupOutcome {
installedVersion: string | null;
/** The install already there was kept: nothing was sent. */
reused: boolean;
+ /** The program this job replaced stays as `~/.local/bin/biorouter-crew.previous`. */
+ previousKept: boolean;
+ /** The version of the program this job replaced (`upgraded_from.version`), when it named one. */
+ replacedVersion: string | null;
/** A host job: what the start commands printed, read as `/crew/host/start` reads it. */
host: HostStartResult | null;
/** A host job: the exact start command text it ran, for the Host dialog's tripwire. */
@@ -174,9 +178,12 @@ function stagesFrom(wire: unknown): SetupStage[] {
function outcomeFrom(wire: unknown): SetupOutcome | null {
if (!isRecord(wire)) return null;
+ const replaced = isRecord(wire.upgraded_from) ? text(wire.upgraded_from.version) : null;
return {
installedVersion: filled(wire.installed_version),
reused: wire.reused === true,
+ previousKept: wire.previous_kept === true,
+ replacedVersion: replaced && VERSION.test(replaced) ? replaced : null,
host: hostStartResultFrom(wire.host),
hostCommand: text(wire.host_command),
connectionId: filled(wire.connection_id),