mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
180 lines
6 KiB
TypeScript
180 lines
6 KiB
TypeScript
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
classifyInstallerObservation,
|
|
installerObservationRoot,
|
|
normalizeUpdateObservationChannel,
|
|
observePendingInstallerApplyAttempts,
|
|
type InstallerObservationSummary,
|
|
} from '../src/update-apply-observations.js';
|
|
|
|
function pendingSummary(overrides: Partial<InstallerObservationSummary> = {}): InstallerObservationSummary {
|
|
return {
|
|
arch: 'x64',
|
|
artifactType: 'installer',
|
|
attemptedAt: '2026-05-20T00:00:00.000Z',
|
|
channel: 'beta',
|
|
flowId: 'flow-1',
|
|
fromVersion: '0.8.0-beta.5',
|
|
kind: 'installer_apply_observation',
|
|
namespace: 'release-beta-win',
|
|
platform: 'win32',
|
|
reason: 'installer_open_requested',
|
|
result: 'pending',
|
|
schemaVersion: 1,
|
|
toVersion: '0.8.0-beta.6',
|
|
updatedAt: '2026-05-20T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function writeSummary(dataRoot: string, summary: InstallerObservationSummary): Promise<string> {
|
|
const filePath = path.join(installerObservationRoot(dataRoot), summary.flowId, 'summary.json');
|
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
await writeFile(filePath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
|
|
return filePath;
|
|
}
|
|
|
|
describe('update apply observations', () => {
|
|
it('classifies next-launch version matches and unchanged versions', () => {
|
|
const now = new Date('2026-05-20T00:03:00.000Z');
|
|
expect(classifyInstallerObservation(pendingSummary(), {
|
|
currentChannel: 'beta',
|
|
currentVersion: '0.8.0-beta.6',
|
|
namespace: 'release-beta-win',
|
|
now,
|
|
})).toMatchObject({
|
|
elapsedBucket: 'lt_5m',
|
|
reason: 'app_version_matches',
|
|
result: 'success',
|
|
});
|
|
|
|
expect(classifyInstallerObservation(pendingSummary(), {
|
|
currentChannel: 'beta',
|
|
currentVersion: '0.8.0-beta.5',
|
|
namespace: 'release-beta-win',
|
|
now,
|
|
})).toMatchObject({
|
|
reason: 'app_version_unchanged',
|
|
result: 'not_applied',
|
|
});
|
|
});
|
|
|
|
it('classifies mismatched runtime identity without using raw paths or errors', () => {
|
|
expect(classifyInstallerObservation(pendingSummary(), {
|
|
currentChannel: 'stable',
|
|
currentVersion: '0.8.0-beta.6',
|
|
namespace: 'release-beta-win',
|
|
now: new Date('2026-05-20T00:03:00.000Z'),
|
|
})).toMatchObject({
|
|
reason: 'identity_mismatch',
|
|
result: 'unknown',
|
|
});
|
|
});
|
|
|
|
it('normalizes nightly and preview observation channels without collapsing them to beta', () => {
|
|
expect(normalizeUpdateObservationChannel('0.8.0.nightly.2')).toBe('nightly');
|
|
expect(normalizeUpdateObservationChannel('0.8.0-preview.2')).toBe('preview');
|
|
expect(normalizeUpdateObservationChannel('0.8.0', 'nightly')).toBe('nightly');
|
|
});
|
|
|
|
it('submits one sanitized analytics event and marks delivery metadata', async () => {
|
|
const dataRoot = await mkdtemp(path.join(tmpdir(), 'od-update-observe-'));
|
|
const capture = vi.fn();
|
|
try {
|
|
const summaryPath = await writeSummary(dataRoot, pendingSummary());
|
|
|
|
await expect(observePendingInstallerApplyAttempts({
|
|
analytics: { capture },
|
|
appVersion: '0.8.0-beta.6',
|
|
currentChannel: 'beta',
|
|
currentVersion: '0.8.0-beta.6',
|
|
dataRoot,
|
|
env: { POSTHOG_KEY: 'ph_test' },
|
|
namespace: 'release-beta-win',
|
|
now: () => new Date('2026-05-20T00:03:00.000Z'),
|
|
readConfig: async () => ({
|
|
installationId: 'install-1',
|
|
telemetry: { metrics: true },
|
|
}),
|
|
})).resolves.toEqual({ observed: 1 });
|
|
|
|
expect(capture).toHaveBeenCalledTimes(1);
|
|
expect(capture.mock.calls[0]?.[0]).toMatchObject({
|
|
eventName: 'update_apply_observed',
|
|
appVersion: '0.8.0-beta.6',
|
|
insertId: 'update_apply_observed:flow-1',
|
|
properties: {
|
|
arch: 'x64',
|
|
artifact_type: 'installer',
|
|
channel: 'beta',
|
|
elapsed_bucket: 'lt_5m',
|
|
flow_id: 'flow-1',
|
|
from_version: '0.8.0-beta.5',
|
|
namespace: 'release-beta-win',
|
|
platform: 'win32',
|
|
reason: 'app_version_matches',
|
|
result: 'success',
|
|
to_version: '0.8.0-beta.6',
|
|
},
|
|
});
|
|
const updated = JSON.parse(await readFile(summaryPath, 'utf8')) as Record<string, unknown>;
|
|
expect(updated).toMatchObject({
|
|
delivery: {
|
|
eventName: 'update_apply_observed',
|
|
insertId: 'update_apply_observed:flow-1',
|
|
status: 'submitted',
|
|
},
|
|
reason: 'app_version_matches',
|
|
result: 'success',
|
|
});
|
|
|
|
await observePendingInstallerApplyAttempts({
|
|
analytics: { capture },
|
|
appVersion: '0.8.0-beta.6',
|
|
currentVersion: '0.8.0-beta.6',
|
|
dataRoot,
|
|
env: { POSTHOG_KEY: 'ph_test' },
|
|
namespace: 'release-beta-win',
|
|
});
|
|
expect(capture).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
await rm(dataRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it('marks no-consent observations without submitting analytics', async () => {
|
|
const dataRoot = await mkdtemp(path.join(tmpdir(), 'od-update-observe-'));
|
|
const capture = vi.fn();
|
|
try {
|
|
const summaryPath = await writeSummary(dataRoot, pendingSummary());
|
|
await observePendingInstallerApplyAttempts({
|
|
analytics: { capture },
|
|
appVersion: '0.8.0-beta.6',
|
|
currentVersion: '0.8.0-beta.6',
|
|
dataRoot,
|
|
env: { POSTHOG_KEY: 'ph_test' },
|
|
namespace: 'release-beta-win',
|
|
readConfig: async () => ({
|
|
installationId: null,
|
|
telemetry: { metrics: false },
|
|
}),
|
|
});
|
|
|
|
expect(capture).not.toHaveBeenCalled();
|
|
const updated = JSON.parse(await readFile(summaryPath, 'utf8')) as Record<string, unknown>;
|
|
expect(updated).toMatchObject({
|
|
delivery: {
|
|
eventName: 'update_apply_observed',
|
|
status: 'skipped_no_consent',
|
|
},
|
|
});
|
|
} finally {
|
|
await rm(dataRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|