feat: add bilingual release notes
This commit is contained in:
@@ -73,11 +73,12 @@ describe('ApplicationSettingsStore', () => {
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
version: 4,
|
||||
version: 5,
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
})
|
||||
expect(
|
||||
(await readdir(directory)).filter((name) => name.endsWith('.tmp'))
|
||||
@@ -166,6 +167,35 @@ describe('ApplicationSettingsStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('migrates version 4 settings with no release notes acknowledged', async () => {
|
||||
const { filePath, store } = await createStore()
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
version: 4,
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'narrative'
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(store.getLastSeenReleaseNotesVersion()).resolves.toBeNull()
|
||||
await store.setLastSeenReleaseNotesVersion('0.8.18')
|
||||
await expect(
|
||||
new ApplicationSettingsStore(filePath).getLastSeenReleaseNotesVersion()
|
||||
).resolves.toBe('0.8.18')
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
version: 5,
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'narrative',
|
||||
lastSeenReleaseNotesVersion: '0.8.18'
|
||||
})
|
||||
})
|
||||
|
||||
it('strictly rejects incomplete full settings', () => {
|
||||
for (const input of [
|
||||
{},
|
||||
@@ -290,11 +320,12 @@ describe('ApplicationSettingsStore', () => {
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})
|
||||
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
|
||||
version: 4,
|
||||
version: 5,
|
||||
checkUpdatesOnStartup: false,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -13,13 +13,14 @@ import {
|
||||
applicationSettingsUpdateSchema,
|
||||
type ApplicationSettings
|
||||
} from '../shared/application-settings-contracts'
|
||||
import { releaseVersionSchema } from '../shared/release-notes-contracts'
|
||||
export {
|
||||
applicationSettingsSchema,
|
||||
applicationSettingsUpdateSchema
|
||||
} from '../shared/application-settings-contracts'
|
||||
export type { ApplicationSettings } from '../shared/application-settings-contracts'
|
||||
|
||||
const CURRENT_SETTINGS_VERSION = 4
|
||||
const CURRENT_SETTINGS_VERSION = 5
|
||||
|
||||
const legacyStoredApplicationSettingsSchema = z
|
||||
.object({
|
||||
@@ -45,9 +46,16 @@ const versionThreeStoredApplicationSettingsSchema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
const versionFourStoredApplicationSettingsSchema = applicationSettingsSchema
|
||||
.extend({
|
||||
version: z.literal(4)
|
||||
})
|
||||
.strict()
|
||||
|
||||
const storedApplicationSettingsSchema = applicationSettingsSchema
|
||||
.extend({
|
||||
version: z.literal(CURRENT_SETTINGS_VERSION)
|
||||
version: z.literal(CURRENT_SETTINGS_VERSION),
|
||||
lastSeenReleaseNotesVersion: releaseVersionSchema.nullable()
|
||||
})
|
||||
.strict()
|
||||
|
||||
@@ -73,6 +81,7 @@ function isMissingFile(error: unknown): boolean {
|
||||
|
||||
export class ApplicationSettingsStore {
|
||||
private settings?: StoredApplicationSettings
|
||||
private settingsLoad?: Promise<StoredApplicationSettings>
|
||||
private updateQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
constructor(private readonly filePath: string) {}
|
||||
@@ -97,6 +106,15 @@ export class ApplicationSettingsStore {
|
||||
if (this.settings) {
|
||||
return this.settings
|
||||
}
|
||||
if (!this.settingsLoad) {
|
||||
this.settingsLoad = this.readStored().finally(() => {
|
||||
this.settingsLoad = undefined
|
||||
})
|
||||
}
|
||||
return this.settingsLoad
|
||||
}
|
||||
|
||||
private async readStored(): Promise<StoredApplicationSettings> {
|
||||
try {
|
||||
const contents = await readFile(this.filePath, 'utf8')
|
||||
let parsed: unknown
|
||||
@@ -106,19 +124,31 @@ export class ApplicationSettingsStore {
|
||||
await this.isolateCorruptFile()
|
||||
this.settings = {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
lastSeenReleaseNotesVersion: null,
|
||||
...defaultApplicationSettings
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
const result = storedApplicationSettingsSchema.safeParse(parsed)
|
||||
if (!result.success) {
|
||||
const versionFourResult =
|
||||
versionFourStoredApplicationSettingsSchema.safeParse(parsed)
|
||||
if (versionFourResult.success) {
|
||||
this.settings = {
|
||||
...versionFourResult.data,
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
lastSeenReleaseNotesVersion: null
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
const versionThreeResult =
|
||||
versionThreeStoredApplicationSettingsSchema.safeParse(parsed)
|
||||
if (versionThreeResult.success) {
|
||||
this.settings = {
|
||||
...versionThreeResult.data,
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
magicNoteCommentFormat: 'combined'
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
@@ -129,7 +159,8 @@ export class ApplicationSettingsStore {
|
||||
...versionTwoResult.data,
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
@@ -142,13 +173,15 @@ export class ApplicationSettingsStore {
|
||||
legacyResult.data.checkUpdatesOnStartup,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
magicNoteCommentFormat: 'combined',
|
||||
lastSeenReleaseNotesVersion: null
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
await this.isolateCorruptFile()
|
||||
this.settings = {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
lastSeenReleaseNotesVersion: null,
|
||||
...defaultApplicationSettings
|
||||
}
|
||||
return this.settings
|
||||
@@ -162,6 +195,7 @@ export class ApplicationSettingsStore {
|
||||
}
|
||||
this.settings = {
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
lastSeenReleaseNotesVersion: null,
|
||||
...defaultApplicationSettings
|
||||
}
|
||||
}
|
||||
@@ -178,6 +212,32 @@ export class ApplicationSettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
async getLastSeenReleaseNotesVersion(): Promise<string | null> {
|
||||
return (await this.loadStored()).lastSeenReleaseNotesVersion
|
||||
}
|
||||
|
||||
private async persist(next: StoredApplicationSettings): Promise<void> {
|
||||
await mkdir(dirname(this.filePath), { recursive: true })
|
||||
const temporaryPath =
|
||||
`${this.filePath}.${process.pid}.` +
|
||||
`${randomBytes(6).toString('hex')}.tmp`
|
||||
try {
|
||||
await writeFile(
|
||||
temporaryPath,
|
||||
`${JSON.stringify(next, null, 2)}\n`,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
mode: 0o600,
|
||||
flag: 'wx'
|
||||
}
|
||||
)
|
||||
await rename(temporaryPath, this.filePath)
|
||||
} finally {
|
||||
await rm(temporaryPath, { force: true })
|
||||
}
|
||||
this.settings = next
|
||||
}
|
||||
|
||||
update(input: unknown): Promise<ApplicationSettings> {
|
||||
const operation = this.updateQueue.then(async () => {
|
||||
const updates = applicationSettingsUpdateSchema.parse(input)
|
||||
@@ -187,25 +247,7 @@ export class ApplicationSettingsStore {
|
||||
...updates,
|
||||
version: CURRENT_SETTINGS_VERSION
|
||||
}
|
||||
await mkdir(dirname(this.filePath), { recursive: true })
|
||||
const temporaryPath =
|
||||
`${this.filePath}.${process.pid}.` +
|
||||
`${randomBytes(6).toString('hex')}.tmp`
|
||||
try {
|
||||
await writeFile(
|
||||
temporaryPath,
|
||||
`${JSON.stringify(next, null, 2)}\n`,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
mode: 0o600,
|
||||
flag: 'wx'
|
||||
}
|
||||
)
|
||||
await rename(temporaryPath, this.filePath)
|
||||
} finally {
|
||||
await rm(temporaryPath, { force: true })
|
||||
}
|
||||
this.settings = next
|
||||
await this.persist(next)
|
||||
return {
|
||||
checkUpdatesOnStartup: next.checkUpdatesOnStartup,
|
||||
magicNotesEnabled: next.magicNotesEnabled,
|
||||
@@ -219,4 +261,24 @@ export class ApplicationSettingsStore {
|
||||
)
|
||||
return operation
|
||||
}
|
||||
|
||||
setLastSeenReleaseNotesVersion(version: unknown): Promise<void> {
|
||||
const operation = this.updateQueue.then(async () => {
|
||||
const parsedVersion = releaseVersionSchema.parse(version)
|
||||
const current = await this.loadStored()
|
||||
if (current.lastSeenReleaseNotesVersion === parsedVersion) {
|
||||
return
|
||||
}
|
||||
await this.persist({
|
||||
...current,
|
||||
version: CURRENT_SETTINGS_VERSION,
|
||||
lastSeenReleaseNotesVersion: parsedVersion
|
||||
})
|
||||
})
|
||||
this.updateQueue = operation.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
)
|
||||
return operation
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -71,6 +71,7 @@ import { DocumentParsingSettingsStore } from './document-parsing-settings-store'
|
||||
import { DocumentOcrModelManager } from './document-ocr-model-manager'
|
||||
import { DocumentOcrBroker } from './document-ocr-broker'
|
||||
import { DocumentParsingService } from './document-parsing-service'
|
||||
import { ReleaseNotesService } from './release-notes-service'
|
||||
|
||||
const shortcut = 'CommandOrControl+Shift+Space'
|
||||
const mainModuleDirectory = dirname(fileURLToPath(import.meta.url))
|
||||
@@ -346,6 +347,13 @@ if (hasSingleInstanceLock) {
|
||||
const applicationSettingsStore = new ApplicationSettingsStore(
|
||||
join(app.getPath('userData'), 'application-settings.json')
|
||||
)
|
||||
const releaseNotesService = new ReleaseNotesService({
|
||||
currentVersion: app.getVersion(),
|
||||
filePath: app.isPackaged
|
||||
? join(process.resourcesPath, 'release-notes.json')
|
||||
: join(app.getAppPath(), 'resources', 'release-notes.json'),
|
||||
settingsStore: applicationSettingsStore
|
||||
})
|
||||
const documentParsingSettingsStore =
|
||||
new DocumentParsingSettingsStore(
|
||||
join(app.getPath('userData'), 'document-parsing-settings.json')
|
||||
@@ -545,7 +553,8 @@ if (hasSingleInstanceLock) {
|
||||
launchWechatSidecar,
|
||||
documentParsingService,
|
||||
documentOcrModelManager,
|
||||
documentOcrBroker
|
||||
documentOcrBroker,
|
||||
releaseNotesService
|
||||
)
|
||||
loadMainWindow(mainWindow)
|
||||
|
||||
|
||||
+25
-1
@@ -66,6 +66,7 @@ import {
|
||||
weComChannelSettingsInputSchema
|
||||
} from '../shared/channel-settings-contracts'
|
||||
import { applicationSettingsUpdateSchema } from '../shared/application-settings-contracts'
|
||||
import { releaseNotesAcknowledgeSchema } from '../shared/release-notes-contracts'
|
||||
import {
|
||||
speechModelActionInputSchema,
|
||||
speechModelSelectionInputSchema
|
||||
@@ -189,6 +190,7 @@ import type { EmbeddingIndexCoordinator } from './knowledge/embedding-index-coor
|
||||
import type { DocumentParsingService } from './document-parsing-service'
|
||||
import type { DocumentOcrModelManager } from './document-ocr-model-manager'
|
||||
import type { DocumentOcrBroker } from './document-ocr-broker'
|
||||
import type { ReleaseNotesService } from './release-notes-service'
|
||||
import { OpenAIEmbeddingClient } from './knowledge/openai-embedding-client'
|
||||
import {
|
||||
magicNotePlainText,
|
||||
@@ -600,7 +602,8 @@ export function registerIpcHandlers(
|
||||
launchWechatSidecar?: WechatSidecarLauncher,
|
||||
documentParsingService?: DocumentParsingService,
|
||||
documentOcrModelManager?: DocumentOcrModelManager,
|
||||
documentOcrBroker?: DocumentOcrBroker
|
||||
documentOcrBroker?: DocumentOcrBroker,
|
||||
releaseNotesService?: ReleaseNotesService
|
||||
): () => Promise<void> {
|
||||
const activeRequests = new Map<string, AbortController>()
|
||||
const pendingAgentQuestions = new Map<
|
||||
@@ -2742,6 +2745,27 @@ export function registerIpcHandlers(
|
||||
await shell.openExternal(GOODBUDDY_RELEASES_URL)
|
||||
})
|
||||
|
||||
ipcMain.handle(ipcChannels.releaseNotesGetPending, (event) => {
|
||||
assertTrustedSender(event, window)
|
||||
if (!releaseNotesService) {
|
||||
throw new Error('版本更新说明服务不可用')
|
||||
}
|
||||
return releaseNotesService.getPending()
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
ipcChannels.releaseNotesAcknowledge,
|
||||
async (event, input: unknown) => {
|
||||
assertTrustedSender(event, window)
|
||||
if (!releaseNotesService) {
|
||||
throw new Error('版本更新说明服务不可用')
|
||||
}
|
||||
await releaseNotesService.acknowledge(
|
||||
releaseNotesAcknowledgeSchema.parse(input)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const requireEmbeddingProvider = async (): Promise<OpenAIEmbeddingClient> => {
|
||||
const settings = await settingsStore.getResolvedSettings()
|
||||
if (!settings.knowledgeEmbeddingEnabled) {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { ApplicationSettingsStore } from './application-settings-store'
|
||||
import { ReleaseNotesService } from './release-notes-service'
|
||||
|
||||
const temporaryDirectories: string[] = []
|
||||
|
||||
const localizedNotes = (label: string) => ({
|
||||
'zh-CN': {
|
||||
features: [`${label} 功能`],
|
||||
fixes: [`${label} 修复`]
|
||||
},
|
||||
'en-US': {
|
||||
features: [`${label} feature`],
|
||||
fixes: [`${label} fix`]
|
||||
}
|
||||
})
|
||||
|
||||
async function createService(
|
||||
currentVersion: string
|
||||
): Promise<{
|
||||
filePath: string
|
||||
service: ReleaseNotesService
|
||||
settingsStore: ApplicationSettingsStore
|
||||
}> {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-release-notes-'))
|
||||
temporaryDirectories.push(directory)
|
||||
const filePath = join(directory, 'release-notes.json')
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
formatVersion: 1,
|
||||
releases: [
|
||||
{
|
||||
version: '0.8.12',
|
||||
releasedAt: '2026-08-04',
|
||||
notes: localizedNotes('0.8.12')
|
||||
},
|
||||
{
|
||||
version: '0.8.18',
|
||||
releasedAt: '2026-08-11',
|
||||
notes: localizedNotes('0.8.18')
|
||||
}
|
||||
]
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
const settingsStore = new ApplicationSettingsStore(
|
||||
join(directory, 'application-settings.json')
|
||||
)
|
||||
return {
|
||||
filePath,
|
||||
settingsStore,
|
||||
service: new ReleaseNotesService({
|
||||
currentVersion,
|
||||
filePath,
|
||||
settingsStore
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories.splice(0).map((directory) =>
|
||||
rm(directory, { recursive: true, force: true })
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
describe('ReleaseNotesService', () => {
|
||||
it('shows only the current release on a fresh installation', async () => {
|
||||
const { service } = await createService('0.8.18')
|
||||
|
||||
await expect(service.getPending()).resolves.toMatchObject({
|
||||
currentVersion: '0.8.18',
|
||||
releases: [{ version: '0.8.18' }]
|
||||
})
|
||||
})
|
||||
|
||||
it('shows every unseen release through the current version', async () => {
|
||||
const { service, settingsStore } = await createService('0.8.18')
|
||||
await settingsStore.setLastSeenReleaseNotesVersion('0.8.11')
|
||||
|
||||
await expect(service.getPending()).resolves.toMatchObject({
|
||||
releases: [{ version: '0.8.12' }, { version: '0.8.18' }]
|
||||
})
|
||||
})
|
||||
|
||||
it('persists acknowledgement and does not show the release again', async () => {
|
||||
const { service, settingsStore } = await createService('0.8.18')
|
||||
|
||||
await service.acknowledge({ version: '0.8.18' })
|
||||
|
||||
await expect(service.getPending()).resolves.toEqual({
|
||||
currentVersion: '0.8.18',
|
||||
releases: []
|
||||
})
|
||||
await expect(
|
||||
settingsStore.getLastSeenReleaseNotesVersion()
|
||||
).resolves.toBe('0.8.18')
|
||||
})
|
||||
|
||||
it('rejects acknowledgement for another or unknown version', async () => {
|
||||
const { service } = await createService('0.8.18')
|
||||
|
||||
await expect(
|
||||
service.acknowledge({ version: '0.8.12' })
|
||||
).rejects.toThrow('Only the current release notes can be acknowledged')
|
||||
await expect(
|
||||
service.acknowledge({ version: '0.8.19' })
|
||||
).rejects.toThrow('Only the current release notes can be acknowledged')
|
||||
})
|
||||
|
||||
it('does not reopen release notes after an application downgrade', async () => {
|
||||
const { service, settingsStore } = await createService('0.8.12')
|
||||
await settingsStore.setLastSeenReleaseNotesVersion('0.8.18')
|
||||
|
||||
await expect(service.getPending()).resolves.toEqual({
|
||||
currentVersion: '0.8.12',
|
||||
releases: []
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects an oversized release-notes resource with a bounded read', async () => {
|
||||
const { filePath, service } = await createService('0.8.18')
|
||||
await writeFile(filePath, ' '.repeat(128 * 1024 + 1), 'utf8')
|
||||
|
||||
await expect(service.getPending()).rejects.toThrow(
|
||||
'Release notes exceed the size limit'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import { open } from 'node:fs/promises'
|
||||
import {
|
||||
releaseNotesAcknowledgeSchema,
|
||||
releaseNotesFileSchema,
|
||||
type ReleaseNote,
|
||||
type ReleaseNotesSnapshot
|
||||
} from '../shared/release-notes-contracts'
|
||||
import type { ApplicationSettingsStore } from './application-settings-store'
|
||||
import { compareStrictSemVer } from './version-checker'
|
||||
|
||||
const maximumReleaseNotesBytes = 128 * 1024
|
||||
|
||||
export class ReleaseNotesService {
|
||||
private releases?: ReleaseNote[]
|
||||
private releaseLoad?: Promise<ReleaseNote[]>
|
||||
|
||||
constructor(
|
||||
private readonly dependencies: {
|
||||
currentVersion: string
|
||||
filePath: string
|
||||
settingsStore: ApplicationSettingsStore
|
||||
}
|
||||
) {}
|
||||
|
||||
private async loadReleases(): Promise<ReleaseNote[]> {
|
||||
if (this.releases) {
|
||||
return this.releases
|
||||
}
|
||||
if (!this.releaseLoad) {
|
||||
this.releaseLoad = this.readReleases().finally(() => {
|
||||
this.releaseLoad = undefined
|
||||
})
|
||||
}
|
||||
return this.releaseLoad
|
||||
}
|
||||
|
||||
private async readReleases(): Promise<ReleaseNote[]> {
|
||||
const handle = await open(this.dependencies.filePath, 'r')
|
||||
try {
|
||||
const buffer = Buffer.alloc(maximumReleaseNotesBytes + 1)
|
||||
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0)
|
||||
if (bytesRead > maximumReleaseNotesBytes) {
|
||||
throw new Error('Release notes exceed the size limit')
|
||||
}
|
||||
const parsed = releaseNotesFileSchema.parse(
|
||||
JSON.parse(buffer.toString('utf8', 0, bytesRead)) as unknown
|
||||
)
|
||||
this.releases = [...parsed.releases].sort((left, right) =>
|
||||
compareStrictSemVer(left.version, right.version)
|
||||
)
|
||||
return this.releases
|
||||
} finally {
|
||||
await handle.close()
|
||||
}
|
||||
}
|
||||
|
||||
async getPending(): Promise<ReleaseNotesSnapshot> {
|
||||
const releases = await this.loadReleases()
|
||||
const currentVersion = this.dependencies.currentVersion
|
||||
const lastSeenVersion =
|
||||
await this.dependencies.settingsStore.getLastSeenReleaseNotesVersion()
|
||||
const pending = releases.filter((release) => {
|
||||
const comparedWithCurrent = compareStrictSemVer(
|
||||
release.version,
|
||||
currentVersion
|
||||
)
|
||||
if (comparedWithCurrent > 0) {
|
||||
return false
|
||||
}
|
||||
return lastSeenVersion
|
||||
? compareStrictSemVer(release.version, lastSeenVersion) > 0
|
||||
: comparedWithCurrent === 0
|
||||
})
|
||||
return {
|
||||
currentVersion,
|
||||
releases: pending
|
||||
}
|
||||
}
|
||||
|
||||
async acknowledge(input: unknown): Promise<void> {
|
||||
const { version } = releaseNotesAcknowledgeSchema.parse(input)
|
||||
if (version !== this.dependencies.currentVersion) {
|
||||
throw new Error('Only the current release notes can be acknowledged')
|
||||
}
|
||||
const releases = await this.loadReleases()
|
||||
if (!releases.some((release) => release.version === version)) {
|
||||
throw new Error('Current release notes are unavailable')
|
||||
}
|
||||
await this.dependencies.settingsStore.setLastSeenReleaseNotesVersion(
|
||||
version
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { releaseNotesFileSchema } from '../shared/release-notes-contracts'
|
||||
|
||||
describe('packaged release notes', () => {
|
||||
it('contains matching bounded Chinese and English content', async () => {
|
||||
const source = JSON.parse(
|
||||
await readFile(
|
||||
join(process.cwd(), 'resources', 'release-notes.json'),
|
||||
'utf8'
|
||||
)
|
||||
) as unknown
|
||||
const parsed = releaseNotesFileSchema.parse(source)
|
||||
|
||||
expect(parsed.releases).toContainEqual(
|
||||
expect.objectContaining({ version: '0.8.18' })
|
||||
)
|
||||
for (const release of parsed.releases) {
|
||||
expect(release.notes['zh-CN'].features).toHaveLength(
|
||||
release.notes['en-US'].features.length
|
||||
)
|
||||
expect(release.notes['zh-CN'].fixes).toHaveLength(
|
||||
release.notes['en-US'].fixes.length
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user