1import { EnvironmentId } from "@t3tools/contracts";
2import type { ServerUpdateState } from "@t3tools/client-runtime/state/server";
3import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
4
5const branding = vi.hoisted(() => ({ APP_VERSION: "0.0.34" }));
6vi.mock("./branding", () => branding);
7
8import { APP_VERSION } from "./branding";
9import {
10 buildVersionMismatchDismissalKey,
11 dismissServerUpdateFailure,
12 dismissVersionMismatch,
13 isServerUpdateFailureDismissed,
14 isVersionMismatchDismissed,
15 manualServerUpdateCommand,
16 resolveVersionMismatch,
17} from "./versionSkew";
18
19const MISMATCH_HINT =
20 "Version mismatch. Try syncing the client and server to the same T3 Code version.";
21
22describe("versionSkew", () => {
23 it("updates only the proven install and safely quotes its paths", () => {
24 expect(manualServerUpdateCommand("0.0.45", { kind: "npm-global", prefix: "/opt/node" })).toBe(
25 "npm install --global --prefix '/opt/node' t3@0.0.45",
26 );
27 expect(
28 manualServerUpdateCommand("0.0.45", { kind: "npm-global", prefix: "/opt/maria's node" }),
29 ).toBe("npm install --global --prefix '/opt/maria'\\''s node' t3@0.0.45");
30 expect(
31 manualServerUpdateCommand("0.0.45", {
32 kind: "t3-launcher",
33 launcher: "t3",
34 baseDir: "/home/u/.t3",
35 }),
36 ).toBe("'t3' update --base-dir '/home/u/.t3' 0.0.45");
37 expect(
38 manualServerUpdateCommand("0.0.45", {
39 kind: "t3-launcher",
40 launcher: "/opt/maria's bin/t3",
41 baseDir: "/data/t3 home",
42 }),
43 ).toBe("'/opt/maria'\\''s bin/t3' update --base-dir '/data/t3 home' 0.0.45");
44 });
45 beforeEach(() => {
46 branding.APP_VERSION = "0.0.34";
47 });
48
49 it("dismisses only the current failed attempt without clearing its retry state", () => {
50 const failure = {
51 status: "failed",
52 stage: "downloading",
53 fromVersion: "0.0.33",
54 targetVersion: "0.0.34",
55 message: "Download failed.",
56 } as const satisfies ServerUpdateState;
57 const retryFailure = { ...failure };
58 const otherEnvironmentFailure = { ...failure };
59
60 dismissServerUpdateFailure(failure);
61
62 expect(isServerUpdateFailureDismissed(failure)).toBe(true);
63 expect(failure.status).toBe("failed");
64 expect(failure.message).toBe("Download failed.");
65 expect(isServerUpdateFailureDismissed(retryFailure)).toBe(false);
66 expect(isServerUpdateFailureDismissed(otherEnvironmentFailure)).toBe(false);
67 });
68
69 it("returns a mismatch when the server is behind the client", () => {
70 expect(resolveVersionMismatch("0.0.33")).toEqual({
71 clientVersion: "0.0.34",
72 serverVersion: "0.0.33",
73 hint: MISMATCH_HINT,
74 });
75 });
76
77 it("does not warn when the server is ahead of the client", () => {
78 expect(resolveVersionMismatch("0.0.35")).toBeNull();
79 expect(resolveVersionMismatch("9.9.9")).toBeNull();
80 });
81
82 it("does not warn when a nightly and a stable build share a core version", () => {
83 expect(resolveVersionMismatch("0.0.34-nightly.20260818.1124")).toBeNull();
84
85 branding.APP_VERSION = "0.0.34-nightly.20260818.1124";
86 expect(resolveVersionMismatch("0.0.34")).toBeNull();
87 });
88
89 it.each(["0.0.34-nightly.20260823.1124", "0.0.34-nightly.20260824.1124"])(
90 "warns when nightly server %s is behind a nightly client on the same release",
91 (serverVersion) => {
92 branding.APP_VERSION = "0.0.34-nightly.20260824.1125";
93
94 expect(resolveVersionMismatch(serverVersion)).toEqual({
95 clientVersion: "0.0.34-nightly.20260824.1125",
96 serverVersion,
97 hint: MISMATCH_HINT,
98 });
99 },
100 );
101
102 // regression: #7579 versions were compared as strings, not semver
103 it("treats a nightly server built past the client as ahead, not skew", () => {
104 expect(resolveVersionMismatch("0.0.35-nightly.20260818.1124")).toBeNull();
105 });
106
107 it("still warns when a nightly client outruns the server by a release", () => {
108 branding.APP_VERSION = "0.0.35-nightly.20260818.1124";
109
110 expect(resolveVersionMismatch("0.0.34")).toEqual({
111 clientVersion: "0.0.35-nightly.20260818.1124",
112 serverVersion: "0.0.34",
113 hint: MISMATCH_HINT,
114 });
115 });
116
117 it("falls back to string inequality when a version is not semver", () => {
118 expect(resolveVersionMismatch("dev")).toEqual({
119 clientVersion: "0.0.34",
120 serverVersion: "dev",
121 hint: MISMATCH_HINT,
122 });
123
124 branding.APP_VERSION = "dev";
125 expect(resolveVersionMismatch("dev")).toBeNull();
126 expect(resolveVersionMismatch("0.0.34")).toMatchObject({ serverVersion: "0.0.34" });
127 });
128
129 it("keys dismissals by environment, client version, and server version", () => {
130 const environmentId = EnvironmentId.make("environment-dismissal");
131 const key = buildVersionMismatchDismissalKey(environmentId, {
132 clientVersion: APP_VERSION,
133 serverVersion: "9.9.9",
134 });
135
136 expect(isVersionMismatchDismissed(key)).toBe(false);
137
138 dismissVersionMismatch(key);
139
140 expect(isVersionMismatchDismissed(key)).toBe(true);
141 expect(
142 isVersionMismatchDismissed(
143 buildVersionMismatchDismissalKey(environmentId, {
144 clientVersion: APP_VERSION,
145 serverVersion: "9.9.10",
146 }),
147 ),
148 ).toBe(false);
149 });
150});