{"version":3,"file":"interactive-mode-update-check.test.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/interactive-mode-update-check.test.ts"],"names":[],"mappings":"","sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport type { ReleaseChannel } from \"../../config.js\";\nimport { getUpdateInstruction, parseStrictStableVersion, semverGt } from \"../../config.js\";\nimport { SettingsManager } from \"../../core/settings-manager.js\";\nimport { InteractiveMode } from \"./interactive-mode.js\";\n\n// Mock isDevMode for tests that need to exercise the registry fetch path\nvi.mock(\"../../config.js\", async () => {\n\tconst actual = await vi.importActual<typeof import(\"../../config.js\")>(\"../../config.js\");\n\treturn {\n\t\t...actual,\n\t\tisDevMode: vi.fn(() => false),\n\t};\n});\n\n// =============================================================================\n// VU: semverGt\n// =============================================================================\n\ndescribe(\"semverGt\", () => {\n\tit(\"VU11: numeric semantic ordering\", () => {\n\t\t// patch comparison\n\t\texpect(semverGt(\"1.1.10\", \"1.1.9\")).toBe(true);\n\t\texpect(semverGt(\"1.1.9\", \"1.1.10\")).toBe(false);\n\n\t\t// minor comparison\n\t\texpect(semverGt(\"1.10.0\", \"1.9.9\")).toBe(true);\n\t\texpect(semverGt(\"1.9.9\", \"1.10.0\")).toBe(false);\n\n\t\t// major comparison\n\t\texpect(semverGt(\"2.0.0\", \"1.99.99\")).toBe(true);\n\t\texpect(semverGt(\"1.99.99\", \"2.0.0\")).toBe(false);\n\t});\n\n\tit(\"VU11: remote older - 1.2.0 < 1.10.0\", () => {\n\t\texpect(semverGt(\"1.10.0\", \"1.2.0\")).toBe(true);\n\t\texpect(semverGt(\"1.2.0\", \"1.10.0\")).toBe(false);\n\t});\n\n\tit(\"equal versions produce false\", () => {\n\t\texpect(semverGt(\"1.1.8\", \"1.1.8\")).toBe(false);\n\t\texpect(semverGt(\"2.0.0\", \"2.0.0\")).toBe(false);\n\t});\n\n\tit(\"rejects short versions (not strict semver)\", () => {\n\t\texpect(semverGt(\"2\", \"1.0.0\")).toBe(false);\n\t\texpect(semverGt(\"1.1\", \"1.1.0\")).toBe(false);\n\t\texpect(semverGt(\"1.0.0\", \"1.1\")).toBe(false);\n\t});\n\n\tit(\"rejects extra version component\", () => {\n\t\texpect(semverGt(\"1.1.8.1\", \"1.1.8\")).toBe(false);\n\t\texpect(semverGt(\"1.1.8\", \"1.1.8.1\")).toBe(false);\n\t});\n\n\tit(\"rejects leading zeros\", () => {\n\t\texpect(semverGt(\"01.1.9\", \"1.1.8\")).toBe(false);\n\t\texpect(semverGt(\"1.01.9\", \"1.1.8\")).toBe(false);\n\t\texpect(semverGt(\"1.1.09\", \"1.1.8\")).toBe(false);\n\t});\n\n\tit(\"rejects prefixes and suffixes\", () => {\n\t\texpect(semverGt(\"v1.1.9\", \"1.1.8\")).toBe(false);\n\t\texpect(semverGt(\"1.1.9-beta.1\", \"1.1.8\")).toBe(false);\n\t\texpect(semverGt(\"1.1.9+build.1\", \"1.1.8\")).toBe(false);\n\t});\n\n\tit(\"rejects whitespace-only and empty\", () => {\n\t\texpect(semverGt(\" \", \"1.0.0\")).toBe(false);\n\t\texpect(semverGt(\"1.0.0\", \" \")).toBe(false);\n\t\texpect(semverGt(\"\", \"1.0.0\")).toBe(false);\n\t\texpect(semverGt(\"1.0.0\", \"\")).toBe(false);\n\t\texpect(semverGt(\"\", \"\")).toBe(false);\n\t});\n\n\tit(\"handles malformed versions safely\", () => {\n\t\texpect(semverGt(\"abc\", \"1.0.0\")).toBe(false);\n\t\texpect(semverGt(\"1.0.0\", \"abc\")).toBe(false);\n\t});\n});\n\n// =============================================================================\n// VER: parseStrictStableVersion\n// =============================================================================\n\ndescribe(\"parseStrictStableVersion\", () => {\n\t// Acceptance\n\tit(\"VER01: accepts valid strict versions\", () => {\n\t\texpect(parseStrictStableVersion(\"0.0.0\")).toEqual({ major: 0, minor: 0, patch: 0 });\n\t\texpect(parseStrictStableVersion(\"0.1.0\")).toEqual({ major: 0, minor: 1, patch: 0 });\n\t\texpect(parseStrictStableVersion(\"1.1.8\")).toEqual({ major: 1, minor: 1, patch: 8 });\n\t\texpect(parseStrictStableVersion(\"1.1.10\")).toEqual({ major: 1, minor: 1, patch: 10 });\n\t\texpect(parseStrictStableVersion(\"10.20.30\")).toEqual({ major: 10, minor: 20, patch: 30 });\n\t});\n\n\t// Rejection: missing components\n\tit(\"VER03: rejects short versions\", () => {\n\t\texpect(parseStrictStableVersion(\"2\")).toBeNull();\n\t\texpect(parseStrictStableVersion(\"1.1\")).toBeNull();\n\t});\n\n\t// Rejection: extra components\n\tit(\"VER05: rejects extra version component\", () => {\n\t\texpect(parseStrictStableVersion(\"1.1.8.1\")).toBeNull();\n\t});\n\n\t// Rejection: prefixes and suffixes\n\tit(\"VER06: rejects 'v' prefix\", () => {\n\t\texpect(parseStrictStableVersion(\"v1.1.9\")).toBeNull();\n\t});\n\n\tit(\"VER07: rejects prerelease\", () => {\n\t\texpect(parseStrictStableVersion(\"1.1.9-beta.1\")).toBeNull();\n\t});\n\n\tit(\"rejects build metadata\", () => {\n\t\texpect(parseStrictStableVersion(\"1.1.9+build.1\")).toBeNull();\n\t});\n\n\t// Rejection: leading zeros\n\tit(\"VER08: rejects leading zeros\", () => {\n\t\texpect(parseStrictStableVersion(\"01.1.9\")).toBeNull();\n\t\texpect(parseStrictStableVersion(\"1.01.9\")).toBeNull();\n\t\texpect(parseStrictStableVersion(\"1.1.09\")).toBeNull();\n\t});\n\n\t// Rejection: whitespace and empty\n\tit(\"rejects whitespace and empty\", () => {\n\t\texpect(parseStrictStableVersion(\"\")).toBeNull();\n\t\texpect(parseStrictStableVersion(\" \")).toBeNull();\n\t\texpect(parseStrictStableVersion(\" 1.1.8\")).toBeNull();\n\t\texpect(parseStrictStableVersion(\"1.1.8 \")).toBeNull();\n\t});\n\n\t// Rejection: signs\n\tit(\"rejects signs\", () => {\n\t\texpect(parseStrictStableVersion(\"-1.1.0\")).toBeNull();\n\t\texpect(parseStrictStableVersion(\"+1.1.0\")).toBeNull();\n\t});\n});\n\n// =============================================================================\n// VU: getUpdateInstruction\n// =============================================================================\n\ndescribe(\"getUpdateInstruction\", () => {\n\tit(\"VU12: fork channel appends @fork to npm command\", () => {\n\t\tconst instruction = getUpdateInstruction(\"@apholdings/jensen-code\", \"fork\");\n\t\texpect(instruction).toContain(\"npm install -g @apholdings/jensen-code@fork\");\n\t});\n\n\tit(\"VU12: fork channel never produces untagged npm command\", () => {\n\t\tconst instruction = getUpdateInstruction(\"@apholdings/jensen-code\", \"fork\");\n\t\texpect(instruction).not.toBe(\"Run: npm install -g @apholdings/jensen-code\");\n\t});\n\n\tit(\"VU04: latest channel omits tag\", () => {\n\t\tconst instruction = getUpdateInstruction(\"@apholdings/jensen-code\", \"latest\");\n\t\texpect(instruction).toContain(\"npm install -g @apholdings/jensen-code\");\n\t\texpect(instruction).not.toContain(\"@latest\");\n\t});\n\n\tit(\"no channel argument produces untagged command\", () => {\n\t\tconst instruction = getUpdateInstruction(\"@apholdings/jensen-code\");\n\t\texpect(instruction).toContain(\"npm install -g @apholdings/jensen-code\");\n\t\texpect(instruction).not.toContain(\"@latest\");\n\t\texpect(instruction).not.toContain(\"@fork\");\n\t});\n});\n\n// =============================================================================\n// VU: dev mode detection\n// =============================================================================\n\ndescribe(\"isDevMode\", () => {\n\tit(\"VU09: returns true when running from source (src/ exists)\", async () => {\n\t\t// The mock overrides isDevMode, so import the real implementation\n\t\tconst actual = await vi.importActual<typeof import(\"../../config.js\")>(\"../../config.js\");\n\t\texpect(actual.isDevMode()).toBe(true);\n\t});\n});\n\n// =============================================================================\n// VU: releaseChannel resolution\n// =============================================================================\n\ndescribe(\"SettingsManager releaseChannel\", () => {\n\tafterEach(() => {\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\tdelete process.env.JENSEN_RELEASE_CHANNEL;\n\t});\n\n\t// VU01 + VU08: default is fork (Apholdings fork default)\n\tit(\"VU01, VU08: defaults to fork when no env or persisted override\", () => {\n\t\tconst sm = SettingsManager.inMemory({});\n\t\texpect(sm.getReleaseChannel()).toBe(\"fork\");\n\t});\n\n\t// VU03: explicit fork via env\n\tit(\"VU03: explicit fork via PI_RELEASE_CHANNEL env\", () => {\n\t\tconst sm = SettingsManager.inMemory({});\n\t\ttry {\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork\";\n\t\t\texpect(sm.getReleaseChannel()).toBe(\"fork\");\n\t\t} finally {\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// VU04: explicit latest via env\n\tit(\"VU04: explicit latest via PI_RELEASE_CHANNEL env\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"latest\";\n\t\t\texpect(sm.getReleaseChannel()).toBe(\"latest\");\n\t\t} finally {\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// VU05: persisted fork\n\tit(\"VU05: persisted releaseChannel=fork honored when no env\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\texpect(sm.getReleaseChannel()).toBe(\"fork\");\n\t});\n\n\t// VU06: invalid env override fails closed, does NOT fall through to persisted\n\tit(\"VU06: invalid env override (unknown) fails closed, ignores persisted fork\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"unknown\";\n\t\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t\t} finally {\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// Also test empty string env\n\tit(\"VU06: empty string env override fails closed\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"\";\n\t\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t\t} finally {\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// Also test beta env\n\tit(\"VU06: unsupported channel env (beta) fails closed\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"beta\";\n\t\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t\t} finally {\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// VU07: invalid persisted setting fails closed\n\tit(\"VU07: invalid persisted releaseChannel fails closed, no fallback\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"unknown\" as ReleaseChannel });\n\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t});\n\n\t// Env takes precedence over persisted\n\tit(\"JENSEN_RELEASE_CHANNEL env var honored\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.JENSEN_RELEASE_CHANNEL = \"latest\";\n\t\t\texpect(sm.getReleaseChannel()).toBe(\"latest\");\n\t\t} finally {\n\t\t\tdelete process.env.JENSEN_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// ENV01: empty canonical env → invalid, no fallback\n\tit(\"ENV01: empty JENSEN_RELEASE_CHANNEL, PI=fork, persisted=fork → invalid\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.JENSEN_RELEASE_CHANNEL = \"\";\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork\";\n\t\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t\t} finally {\n\t\t\tdelete process.env.JENSEN_RELEASE_CHANNEL;\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// ENV02: whitespace-only canonical env → invalid, no fallback\n\tit(\"ENV02: whitespace JENSEN_RELEASE_CHANNEL, PI=fork → invalid\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.JENSEN_RELEASE_CHANNEL = \"   \";\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork\";\n\t\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t\t} finally {\n\t\t\tdelete process.env.JENSEN_RELEASE_CHANNEL;\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// ENV03: empty legacy env → invalid, no fallback to persisted\n\tit(\"ENV03: JENSEN absent, PI empty, persisted=fork → invalid\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"\";\n\t\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t\t} finally {\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// ENV04: invalid canonical wins over valid legacy\n\tit(\"ENV04: invalid JENSEN_RELEASE_CHANNEL, valid PI → invalid, no fallback\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"fork\" });\n\t\ttry {\n\t\t\tprocess.env.JENSEN_RELEASE_CHANNEL = \"unknown\";\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork\";\n\t\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t\t} finally {\n\t\t\tdelete process.env.JENSEN_RELEASE_CHANNEL;\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// ENV05: JENSEN absent, PI=fork → fork\n\tit(\"ENV05: JENSEN absent, PI=fork → fork\", () => {\n\t\tconst sm = SettingsManager.inMemory({});\n\t\ttry {\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork\";\n\t\t\texpect(sm.getReleaseChannel()).toBe(\"fork\");\n\t\t} finally {\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// ENV06: both env absent, persisted absent → fork\n\tit(\"ENV06: both env absent, persisted absent → fork\", () => {\n\t\tconst sm = SettingsManager.inMemory({});\n\t\texpect(sm.getReleaseChannel()).toBe(\"fork\");\n\t});\n\n\t// Canonical precedence: JENSEN=fork, PI=latest → fork\n\tit(\"JENSEN_RELEASE_CHANNEL takes precedence over PI_RELEASE_CHANNEL\", () => {\n\t\tconst sm = SettingsManager.inMemory({});\n\t\ttry {\n\t\t\tprocess.env.JENSEN_RELEASE_CHANNEL = \"fork\";\n\t\t\tprocess.env.PI_RELEASE_CHANNEL = \"latest\";\n\t\t\texpect(sm.getReleaseChannel()).toBe(\"fork\");\n\t\t} finally {\n\t\t\tdelete process.env.JENSEN_RELEASE_CHANNEL;\n\t\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\t}\n\t});\n\n\t// All explicit sources absent → fork\n\tit(\"all explicit sources absent → fork\", () => {\n\t\tconst sm = SettingsManager.inMemory({});\n\t\texpect(sm.getReleaseChannel()).toBe(\"fork\");\n\t});\n\n\t// Persisted empty string → invalid (validated same as env)\n\tit(\"persisted empty releaseChannel → invalid, no fallback\", () => {\n\t\tconst sm = SettingsManager.inMemory({ releaseChannel: \"\" as ReleaseChannel });\n\t\texpect(sm.getReleaseChannel()).toBeUndefined();\n\t});\n});\n\n// =============================================================================\n// VU: checkForNewVersion production path\n// =============================================================================\n\ntype CheckHarness = {\n\tcheckForNewVersion: () => Promise<string | undefined>;\n\tversion: string;\n};\n\ndescribe(\"checkForNewVersion\", () => {\n\tlet fetchSpy: ReturnType<typeof vi.fn>;\n\tlet originalFetch: typeof global.fetch;\n\n\tbeforeEach(() => {\n\t\toriginalFetch = global.fetch;\n\t\tfetchSpy = vi.fn();\n\t\tglobal.fetch = fetchSpy;\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\tdelete process.env.JENSEN_RELEASE_CHANNEL;\n\t});\n\n\tafterEach(() => {\n\t\tglobal.fetch = originalFetch;\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t\tdelete process.env.JENSEN_RELEASE_CHANNEL;\n\t});\n\n\tfunction createHarness(settings: { releaseChannel?: ReleaseChannel } = {}): CheckHarness {\n\t\tconst sm = SettingsManager.inMemory(settings);\n\t\tconst mockSession = { settingsManager: sm };\n\t\treturn Object.assign(Object.create(InteractiveMode.prototype), {\n\t\t\tversion: \"1.1.8\",\n\t\t\tsession: mockSession,\n\t\t}) as CheckHarness;\n\t}\n\n\t// VU01: The initial reported incident — default fork channel, current == remote\n\tit(\"VU01: reported incident - fork default, current == fork remote, no update\", async () => {\n\t\tconst mode = createHarness({});\n\t\tmode.version = \"1.1.8\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.8\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\texpect(fetchSpy).toHaveBeenCalledWith(\n\t\t\texpect.stringContaining(\"/@apholdings/jensen-code/fork\"),\n\t\t\texpect.any(Object),\n\t\t);\n\t});\n\n\t// VU02: Future default fork update\n\tit(\"VU02: future fork update - default channel, current < fork remote\", async () => {\n\t\tconst mode = createHarness({});\n\t\tmode.version = \"1.1.8\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.9\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBe(\"1.1.9\");\n\t\texpect(fetchSpy).toHaveBeenCalledWith(\n\t\t\texpect.stringContaining(\"/@apholdings/jensen-code/fork\"),\n\t\t\texpect.any(Object),\n\t\t);\n\t});\n\n\t// VU03: Explicit fork channel\n\tit(\"VU03: explicit fork via persisted, current < remote → update shown\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"fork\" });\n\t\tmode.version = \"1.1.7\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.8\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBe(\"1.1.8\");\n\t\texpect(fetchSpy).toHaveBeenCalledWith(\n\t\t\texpect.stringContaining(\"/@apholdings/jensen-code/fork\"),\n\t\t\texpect.any(Object),\n\t\t);\n\t});\n\n\tit(\"VU03: explicit fork via env, current < remote → update shown\", async () => {\n\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork\";\n\t\tconst mode = createHarness({});\n\t\tmode.version = \"1.1.7\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.8\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBe(\"1.1.8\");\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t});\n\n\t// VU04: Explicit latest channel\n\tit(\"VU04: latest channel with real update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\tmode.version = \"1.1.5\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.6\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBe(\"1.1.6\");\n\t\texpect(fetchSpy).toHaveBeenCalledWith(\n\t\t\texpect.stringContaining(\"/@apholdings/jensen-code/latest\"),\n\t\t\texpect.any(Object),\n\t\t);\n\t});\n\n\t// VU06: Invalid environment override — fail closed\n\tit(\"VU06: invalid env (unknown) → no registry query, no update\", async () => {\n\t\tprocess.env.PI_RELEASE_CHANNEL = \"unknown\";\n\t\tconst mode = createHarness({});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\texpect(fetchSpy).not.toHaveBeenCalled();\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t});\n\n\tit(\"VU06: invalid env (shell metacharacters) → no registry query\", async () => {\n\t\tprocess.env.PI_RELEASE_CHANNEL = \"latest;rm\";\n\t\tconst mode = createHarness({});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\texpect(fetchSpy).not.toHaveBeenCalled();\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t});\n\n\tit(\"VU13: env containing slash rejected\", async () => {\n\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork/evil\";\n\t\tconst mode = createHarness({});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\texpect(fetchSpy).not.toHaveBeenCalled();\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t});\n\n\tit(\"VU13: env containing @ rejected\", async () => {\n\t\tprocess.env.PI_RELEASE_CHANNEL = \"@latest\";\n\t\tconst mode = createHarness({});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\texpect(fetchSpy).not.toHaveBeenCalled();\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t});\n\n\tit(\"VU13: env containing space rejected\", async () => {\n\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork latest\";\n\t\tconst mode = createHarness({});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\texpect(fetchSpy).not.toHaveBeenCalled();\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t});\n\n\tit(\"VU13: env containing newline rejected\", async () => {\n\t\tprocess.env.PI_RELEASE_CHANNEL = \"fork\\nlatest\";\n\t\tconst mode = createHarness({});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\texpect(fetchSpy).not.toHaveBeenCalled();\n\t\tdelete process.env.PI_RELEASE_CHANNEL;\n\t});\n\n\t// VU07: Invalid persisted setting — fail closed\n\tit(\"VU07: invalid persisted releaseChannel → no registry query\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"unknown\" as ReleaseChannel });\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\texpect(fetchSpy).not.toHaveBeenCalled();\n\t});\n\n\t// VU09: Development/source mode\n\tit(\"VU09: dev mode always returns undefined, no fetch\", async () => {\n\t\tconst { isDevMode: isDevModeMock } = await import(\"../../config.js\");\n\t\t(isDevModeMock as ReturnType<typeof vi.fn>).mockReturnValue(true);\n\n\t\ttry {\n\t\t\tconst mode = createHarness({});\n\n\t\t\tfetchSpy.mockResolvedValue({\n\t\t\t\tok: true,\n\t\t\t\tjson: async () => ({ version: \"9.9.9\" }),\n\t\t\t});\n\n\t\t\tconst result = await mode.checkForNewVersion();\n\t\t\texpect(result).toBeUndefined();\n\t\t\texpect(fetchSpy).not.toHaveBeenCalled();\n\t\t} finally {\n\t\t\t(isDevModeMock as ReturnType<typeof vi.fn>).mockReturnValue(false);\n\t\t}\n\t});\n\n\t// VU10: Registry failures\n\tit(\"VU10: registry error (non-ok response) - no update, no crash\", async () => {\n\t\tconst mode = createHarness({});\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: false,\n\t\t\tstatus: 500,\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\tit(\"VU10: registry timeout (fetch throws) - no update, no crash\", async () => {\n\t\tconst mode = createHarness({});\n\n\t\tfetchSpy.mockRejectedValue(new Error(\"timeout\"));\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\tit(\"VU10: missing version field - no update\", async () => {\n\t\tconst mode = createHarness({});\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ name: \"some-package\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\tit(\"VU10: malformed version field - no update\", async () => {\n\t\tconst mode = createHarness({});\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"not-a-version\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\tit(\"VU10: null version field - no update\", async () => {\n\t\tconst mode = createHarness({});\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: null }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VU11: Semantic ordering\n\tit(\"VU11: 1.1.10 > 1.1.9 produces update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\tmode.version = \"1.1.9\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.10\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBe(\"1.1.10\");\n\t});\n\n\tit(\"VU11: 1.10.0 > 1.9.9 produces update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\tmode.version = \"1.9.9\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.10.0\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBe(\"1.10.0\");\n\t});\n\n\tit(\"VU11: 1.2.0 < 1.10.0 produces no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\tmode.version = \"1.10.0\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.2.0\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\tit(\"VU11: equal versions produce no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\tmode.version = \"1.1.6\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.6\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VU12: Command integrity\n\tit(\"VU12: fork update command contains @fork\", () => {\n\t\tconst instruction = getUpdateInstruction(\"@apholdings/jensen-code\", \"fork\");\n\t\texpect(instruction).toContain(\"@fork\");\n\t\texpect(instruction).toContain(\"npm install -g\");\n\t});\n\n\t// Regression: reported incident exact scenario\n\tit(\"regression: 1.1.8 on fork vs stale 1.1.6 on latest\", async () => {\n\t\t// fork user running 1.1.8, latest dist-tag is 1.1.6.\n\t\t// Default channel must be fork, so we never see latest's stale version.\n\t\tconst mode = createHarness({});\n\t\tmode.version = \"1.1.8\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.8\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t\t// Must have queried fork, not latest\n\t\texpect(fetchSpy).toHaveBeenCalledWith(\n\t\t\texpect.stringContaining(\"/@apholdings/jensen-code/fork\"),\n\t\t\texpect.any(Object),\n\t\t);\n\t});\n\n\t// VER03: Short remote version → no update\n\tit(\"VER03: short remote version '2' → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"2\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VER04: Short current version → no update (production uses this.version)\n\tit(\"VER04: short remote '1.1' → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VER05: Extra component → no update\n\tit(\"VER05: remote '1.1.8.1' with extra component → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\tmode.version = \"1.1.8\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.8.1\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VER06: 'v' prefix → no update\n\tit(\"VER06: remote 'v1.1.9' → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"v1.1.9\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VER07: prerelease → no update\n\tit(\"VER07: remote '1.1.9-beta.1' → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.9-beta.1\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VER08: leading zero → no update\n\tit(\"VER08: remote '01.1.9' → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"01.1.9\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VER09: Current version malformed, remote valid → no update\n\tit(\"VER09: malformed current version, valid remote → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\t// Simulate a hypothetically corrupted current version\n\t\tmode.version = \"1.1\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.2.0\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\t// VER10: Equal and older versions → no update (valid case)\n\tit(\"VER10: valid equal version → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\tmode.version = \"1.1.8\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.8\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\tit(\"VER10: valid older remote → no update\", async () => {\n\t\tconst mode = createHarness({ releaseChannel: \"latest\" });\n\t\tmode.version = \"1.2.0\";\n\n\t\tfetchSpy.mockResolvedValue({\n\t\t\tok: true,\n\t\t\tjson: async () => ({ version: \"1.1.9\" }),\n\t\t});\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\t});\n\n\tit(\"PI_SKIP_VERSION_CHECK suppresses check\", async () => {\n\t\tconst mode = createHarness({});\n\t\tprocess.env.PI_SKIP_VERSION_CHECK = \"1\";\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\n\t\tdelete process.env.PI_SKIP_VERSION_CHECK;\n\t});\n\n\tit(\"PI_OFFLINE suppresses check\", async () => {\n\t\tconst mode = createHarness({});\n\t\tprocess.env.PI_OFFLINE = \"1\";\n\n\t\tconst result = await mode.checkForNewVersion();\n\t\texpect(result).toBeUndefined();\n\n\t\tdelete process.env.PI_OFFLINE;\n\t});\n});\n"]}