diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc7a675f..d3cb2d413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Last-known-good update state survives watch errors** ([#543](https://github.com/CodesWhat/drydock/issues/543)). A failed watch cycle — registry timeout, rate limit, transient network error — no longer erases the previous successful comparison. The stored `result`, `updateAvailable`, `updateKind`, and current release notes are preserved alongside the recorded error until a later cycle succeeds, so the UI keeps showing the last known update state with the error annotated next to it instead of a blank. +- Portwing WS Welcome frames now report the actual server version — the endpoint had hard-coded `1.5.0` since v1.5.0, so v1.5.1/v1.5.2/v1.6.0-rc.1 servers all identified as `1.5.0` to agents ([#550](https://github.com/CodesWhat/drydock/pull/550) review finding) + ## [1.6.0-rc.1] — 2026-07-15 ### Added diff --git a/app/api/portwing-ws.test.ts b/app/api/portwing-ws.test.ts index 8b5ca8c3d..764dd6c14 100644 --- a/app/api/portwing-ws.test.ts +++ b/app/api/portwing-ws.test.ts @@ -5,6 +5,7 @@ import { createHash, sign as cryptoSign, generateKeyPairSync } from 'node:crypto'; import type { IncomingMessage } from 'node:http'; import type { Socket } from 'node:net'; +import { getVersion } from '../configuration/index.js'; import type { AgentKeyRecord } from '../store/agent-keys.js'; import type { NameBindingRecord } from '../store/name-bindings.js'; import * as nameBindingsStore from '../store/name-bindings.js'; @@ -24,6 +25,7 @@ import { vi.mock('../configuration/index.js', () => ({ getServerConfiguration: vi.fn(() => ({})), + getVersion: vi.fn(() => '9.9.9-test'), })); // Hoisted so tests can assert on the durable-flush call (Fix 1: new-bind @@ -319,6 +321,42 @@ function buildHello( // ---- Tests ---- +test('reports the canonical configuration version in the welcome frame', async () => { + clearNonceCacheForTesting(); + + const { privateKey, pubkeyBase64, keyId } = generateKeyPair(); + const ts = Math.floor(Date.now() / 1000); + const nonce = 'd2b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6'; + const sig = signHello(privateKey, ts, nonce); + const record: AgentKeyRecord = { + keyId, + pubkey: pubkeyBase64, + label: 'test', + createdAt: new Date().toISOString(), + revokedAt: null, + }; + + const { gateway, getUpgradedWs } = createGateway(record); + gateway.handleUpgrade( + createRequest('/api/portwing/ws'), + createMockSocket() as unknown as Socket, + Buffer.alloc(0), + ); + const ws = getUpgradedWs()!; + + sendMessageToGateway(ws, buildHello(keyId, ts, nonce, sig)); + // Bounded wait instead of a fixed sleep — the async Ed25519 verification + // makes frame timing scheduler-dependent under CI load. + await vi.waitFor(() => { + expect(ws.sentMessages.length).toBeGreaterThan(0); + }); + + const welcome = JSON.parse(ws.sentMessages[0]) as { + data: { config: { drydockVersion: string } }; + }; + expect(welcome.data.config.drydockVersion).toBe(getVersion()); +}); + describe('PORTWING_WS_ROUTE_PATTERN', () => { test('matches canonical /api/portwing/ws', () => { expect(PORTWING_WS_ROUTE_PATTERN.test('/api/portwing/ws')).toBe(true); diff --git a/app/api/portwing-ws.ts b/app/api/portwing-ws.ts index 2e4cc6ad5..77172c152 100644 --- a/app/api/portwing-ws.ts +++ b/app/api/portwing-ws.ts @@ -19,7 +19,7 @@ import { type WebSocketLike, } from '../agent/EdgeAgentAdapter.js'; import { getAgent } from '../agent/manager.js'; -import { getServerConfiguration } from '../configuration/index.js'; +import { getServerConfiguration, getVersion } from '../configuration/index.js'; import logger from '../log/index.js'; import * as agentKeys from '../store/agent-keys.js'; import { save as saveStore } from '../store/index.js'; @@ -334,15 +334,15 @@ function sendErrorAndClose( ws.close(closeCode, code); } -// Package version sourced at module init time from the app package.json. -// This is the server-side version operators can verify externally. +// Server version reported in the welcome frame — the canonical getVersion() +// (DD_VERSION override, else package.json), cached after first read. let _drydockVersion: string | undefined; function drydockVersion(): string { if (_drydockVersion !== undefined) { return _drydockVersion; } - // Populated by injectDrydockVersionForTesting() in tests, or from package.json at runtime. - _drydockVersion = '1.5.0'; + // Populated by injectDrydockVersionForTesting() in tests, or from getVersion() at runtime. + _drydockVersion = getVersion(); return _drydockVersion; }