Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 134 additions & 3 deletions apps/desktop/e2e/config-canary.e2e.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import { join } from 'node:path';
import { noop } from 'foxts/noop';
import { wait } from 'foxts/wait';
import type { ElectronApplication } from 'playwright-core';
import type { DistServer, ServerMode } from './config-canary/dist-server.mts';
import { startDistServer, waitForRequest } from './config-canary/dist-server.mts';
import type { DistServer, EmergencyServerMode, ServerMode } from './config-canary/dist-server.mts';
import {
startDistServer,
startEmergencyServer,
waitForRequest,
} from './config-canary/dist-server.mts';
import {
buildDesktopWithBootstrap,
EMERGENCY_PORT,
generateTlsMaterial,
launchApp,
PORT,
Expand All @@ -21,18 +26,21 @@ import { baseline, canary, rollback, rollForward } from './config-canary/fixture
import type { ConfigStateFile } from './config-canary/state-file.mts';
import {
readConfigState,
readEmergencyState,
waitForConfigState,
writeCorruptConfigState,
} from './config-canary/state-file.mts';

const REJECTION_SETTLE_MS = 1000;

let mode: ServerMode = 'offline';
let emergencyMode: EmergencyServerMode = 'offline';

interface Harness {
app: ElectronApplication | null;
readonly caCert: string;
readonly dist: DistServer;
readonly emergency: DistServer;
readonly home: string;
readonly userData: string;
}
Expand All @@ -41,9 +49,12 @@ async function withLaunch(
harness: Harness,
nextMode: ServerMode,
assertion: () => Promise<void>,
nextEmergencyMode: EmergencyServerMode = 'offline',
): Promise<void> {
mode = nextMode;
emergencyMode = nextEmergencyMode;
harness.dist.requests.length = 0;
harness.emergency.requests.length = 0;
harness.app = await launchApp(harness.userData, harness.home, harness.caCert);
try {
await assertion();
Expand Down Expand Up @@ -199,6 +210,124 @@ async function driveScenarios(harness: Harness): Promise<void> {
assertAccepted(state, baseline);
console.log('PASS baseline republication recovers after corrupt-state reset');
});

await withLaunch(
harness,
'offline',
async () => {
const app = assertApp(harness);
const boundary = await refreshBoundary(app);
assert.equal(boundary.report.normal, 'error');
assert(
boundary.report.emergency === 'updated' || boundary.report.emergency === 'not-modified',
);
const emergency = boundary.info.emergency;
assert(emergency);
assert.deepEqual(emergency.disabledFeatures, ['feature.aiAssist']);
assert.equal(emergency.emergencyVersion, '1');
await waitForRequest(
harness.dist,
(request) => request.path === baseline.pointerPath && request.status === 'disconnected',
);
await waitForRequest(
harness.emergency,
(request) => request.mode === 'kill-switch' && request.status === 200,
);
console.log('PASS emergency origin activates kill switch during main-channel outage');
},
'kill-switch',
);

await withLaunch(
harness,
'offline',
async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert(
boundary.report.emergency === 'updated' || boundary.report.emergency === 'not-modified',
);
const emergency = boundary.info.emergency;
assert(emergency);
assert.equal(emergency.emergencyVersion, '2');
assert.equal(emergency.forceMinVersion, '2.4.0');
console.log('PASS newer emergency state replaces the persisted kill switch');
},
'forced-minimum',
);

await withLaunch(harness, 'offline', async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert.equal(boundary.report.emergency, 'error');
const emergency = boundary.info.emergency;
assert(emergency);
assert.equal(emergency.emergencyVersion, '2');
assert.equal(emergency.forceMinVersion, '2.4.0');
console.log('PASS forced minimum survives reconstructed runtime and emergency outage');
});

let releaseRaw = '';
await withLaunch(
harness,
'offline',
async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert(
boundary.report.emergency === 'updated' || boundary.report.emergency === 'not-modified',
);
const emergency = boundary.info.emergency;
assert(emergency);
assert.deepEqual(emergency.disabledFeatures, []);
assert.equal(emergency.emergencyVersion, '3');
releaseRaw = (await readEmergencyState(harness.home))?.raw ?? '';
assert(releaseRaw);
console.log('PASS explicit newer release clears emergency restrictions');
},
'release',
);

await withLaunch(
harness,
'offline',
async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert.equal(boundary.report.emergency, 'error');
await waitForRequest(
harness.emergency,
(request) => request.mode === 'equivocation' && request.status === 200,
);
const emergency = boundary.info.emergency;
assert(emergency);
assert.deepEqual(emergency.disabledFeatures, []);
assert.equal(emergency.emergencyVersion, '3');
assert.equal((await readEmergencyState(harness.home))?.raw, releaseRaw);
console.log('PASS equal-version emergency equivocation cannot replace explicit release');
},
'equivocation',
);

await withLaunch(harness, 'offline', async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert.equal(boundary.report.emergency, 'error');
const emergency = boundary.info.emergency;
assert(emergency);
assert.deepEqual(emergency.disabledFeatures, []);
assert.equal(emergency.emergencyVersion, '3');
assert.equal((await readEmergencyState(harness.home))?.raw, releaseRaw);
console.log('PASS explicit release remains sticky across reconstructed runtime and outage');
});
}

function assertApp(harness: Harness): ElectronApplication {
assert(harness.app);
return harness.app;
}

async function refreshBoundary(app: ElectronApplication) {
const page = await app.firstWindow();
return page.evaluate(async () => {
const report = await window.linkcodeConfig.refresh();
return { info: window.linkcodeConfig.snapshotInfo(), report };
});
}

async function main(): Promise<void> {
Expand All @@ -210,7 +339,8 @@ async function main(): Promise<void> {
const tls = generateTlsMaterial(scratch);
buildDesktopWithBootstrap();
const dist = await startDistServer(tls, PORT, () => mode);
harness = { app: null, caCert: tls.cert, dist, home, userData };
const emergency = await startEmergencyServer(tls, EMERGENCY_PORT, () => emergencyMode);
harness = { app: null, caCert: tls.cert, dist, emergency, home, userData };
await driveScenarios(harness);

console.log(
Expand All @@ -219,6 +349,7 @@ async function main(): Promise<void> {
} finally {
await harness?.app?.close().catch(noop);
harness?.dist.server.close();
harness?.emergency.server.close();
rmSync(scratch, { recursive: true, force: true });
}
}
Expand Down
60 changes: 59 additions & 1 deletion apps/desktop/e2e/config-canary/dist-server.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { PilotFixtureStep } from './fixture.mts';
import {
baseline,
canary,
emergencyBytes,
fixture,
pointerBytes,
rollback,
Expand All @@ -27,9 +28,16 @@ export type ServerMode =
| 'tampered-pointer'
| 'tampered-snapshot';

export type EmergencyServerMode =
| 'equivocation'
| 'forced-minimum'
| 'kill-switch'
| 'offline'
| 'release';

export interface DistRequest {
readonly ifNoneMatch: string | null;
readonly mode: ServerMode;
readonly mode: EmergencyServerMode | ServerMode;
readonly path: string;
readonly status: 200 | 304 | 404 | 'disconnected';
}
Expand Down Expand Up @@ -78,6 +86,7 @@ for (const step of fixture.steps) {
assert(!existing || existing.equals(bytes), `conflicting fixture path ${step.snapshotPath}`);
snapshotArtifacts.set(step.snapshotPath, bytes);
}
const emergencyPath = `/v1/${fixture.target.brandId}/desktop/emergency.json`;

function pointerResponse(mode: ServerMode, path: string): PointerArtifact | null {
if (mode === 'offline') return null;
Expand Down Expand Up @@ -148,6 +157,55 @@ export function startDistServer(
});
}

export function startEmergencyServer(
tls: { cert: string; key: string },
port: number,
getMode: () => EmergencyServerMode,
): Promise<DistServer> {
const requests: DistRequest[] = [];
const server = createServer(
{ cert: readFileSync(tls.cert), key: readFileSync(tls.key) },
(request, response) => {
const mode = getMode();
const path = new URL(request.url ?? '/', `https://127.0.0.1:${port}`).pathname;
const header = request.headers['if-none-match'];
const ifNoneMatch = typeof header === 'string' ? header : null;
if (mode === 'offline') {
requests.push({ ifNoneMatch, mode, path, status: 'disconnected' });
request.socket.destroy();
return;
}
if (path !== emergencyPath) {
requests.push({ ifNoneMatch, mode, path, status: 404 });
response.writeHead(404).end();
return;
}
const name =
mode === 'kill-switch' ? 'killSwitch' : mode === 'forced-minimum' ? 'forcedMinimum' : mode;
const bytes = emergencyBytes(name);
const etag = `"emergency-${mode}"`;
if (ifNoneMatch === etag) {
requests.push({ ifNoneMatch, mode, path, status: 304 });
response.writeHead(304, { etag }).end();
return;
}
requests.push({ ifNoneMatch, mode, path, status: 200 });
response
.writeHead(200, {
'cache-control': 'public, max-age=60, must-revalidate',
'content-length': bytes.byteLength,
'content-type': 'application/json',
etag,
})
.end(bytes);
},
);
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', () => resolve({ requests, server }));
});
}

export function waitForRequest(
server: DistServer,
predicate: (request: DistRequest) => boolean,
Expand Down
7 changes: 4 additions & 3 deletions apps/desktop/e2e/config-canary/electron-app.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { join, resolve as resolvePath } from 'node:path';
import type { ElectronApplication } from 'playwright-core';
import { _electron } from 'playwright-core';

import { fixture } from './fixture.mts';
import { emergencyFixture, fixture } from './fixture.mts';

const require = createRequire(import.meta.url);
const desktopDir = resolvePath(import.meta.dirname, '../..');
const electronBinary = require('electron') as unknown as string;

export const PORT = 44100 + (process.pid % 1000);
export const EMERGENCY_PORT = PORT + 1000;

export function generateTlsMaterial(directory: string): { cert: string; key: string } {
const key = join(directory, 'key.pem');
Expand Down Expand Up @@ -49,8 +50,8 @@ export function buildDesktopWithBootstrap(): void {
brandId: fixture.target.brandId,
channel: fixture.target.channel,
defaults: fixture.bootstrapDefaults,
emergencyEndpoint: null,
emergencyPublicKeys: {},
emergencyEndpoint: `https://127.0.0.1:${EMERGENCY_PORT}`,
emergencyPublicKeys: emergencyFixture.keys.emergency,
endpoint: `https://127.0.0.1:${PORT}`,
maximumSchemaVersion: fixture.maximumSchemaVersion,
publicKeys: fixture.keys,
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/e2e/config-canary/fixture.mts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,28 @@ interface PilotFixture {
readonly target: { brandId: string; channel: string; platform: string };
}

interface EmergencyFixture {
readonly documents: Readonly<
Record<
'equivocation' | 'forcedMinimum' | 'killSwitch' | 'release',
{ readonly document: Readonly<Record<string, unknown>> }
>
>;
readonly keys: { readonly emergency: Readonly<Record<string, string>> };
}

export const fixture = JSON.parse(
readFileSync(join(import.meta.dirname, '../fixtures/pilot-e2e-v1.json'), 'utf8'),
) as PilotFixture;
export const emergencyFixture = JSON.parse(
readFileSync(
join(
import.meta.dirname,
'../../../../packages/foundation/common/src/config/__fixtures__/emergency-handoff-v1.json',
),
'utf8',
),
) as EmergencyFixture;

function fixtureStep(name: PilotFixtureStep['name']): PilotFixtureStep {
const found = fixture.steps.find((step) => step.name === name);
Expand All @@ -45,6 +64,9 @@ export function pointerBytes(step: PilotFixtureStep): Buffer {
export function snapshotBytes(step: PilotFixtureStep): Buffer {
return Buffer.from(step.snapshotBase64Url, 'base64url');
}
export function emergencyBytes(name: keyof EmergencyFixture['documents']): Buffer {
return Buffer.from(JSON.stringify(emergencyFixture.documents[name].document), 'utf8');
}

// Same byte length keeps the JSON canonical while invalidating the Ed25519 signature.
export const tamperedPointer = Buffer.from(
Expand Down
19 changes: 15 additions & 4 deletions apps/desktop/e2e/config-canary/state-file.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from 'node:path';
import { asyncRetry } from 'foxts/async-retry';

const STORAGE_KEY = 'linkcode-config:v1:normal:acme:desktop:canary';
const EMERGENCY_STORAGE_KEY = 'linkcode-config:v1:emergency:acme:desktop';

export interface ConfigState {
readonly highWater?: { readonly payloadSha256: string; readonly version: string };
Expand All @@ -17,19 +18,29 @@ export interface ConfigStateFile {
readonly value: ConfigState;
}

function statePath(home: string): string {
function statePath(home: string, storageKey: string): string {
return join(
home,
'.config',
'LinkCode Development',
'config',
`${Buffer.from(STORAGE_KEY).toString('base64url')}.json`,
`${Buffer.from(storageKey).toString('base64url')}.json`,
);
}

export async function readConfigState(home: string): Promise<ConfigStateFile | null> {
try {
const raw = await readFile(statePath(home), 'utf8');
const raw = await readFile(statePath(home, STORAGE_KEY), 'utf8');
return { raw, value: JSON.parse(raw) as ConfigState };
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
throw error;
}
}

export async function readEmergencyState(home: string): Promise<ConfigStateFile | null> {
try {
const raw = await readFile(statePath(home, EMERGENCY_STORAGE_KEY), 'utf8');
return { raw, value: JSON.parse(raw) as ConfigState };
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
Expand Down Expand Up @@ -58,5 +69,5 @@ export function waitForConfigState(
}

export function writeCorruptConfigState(home: string): Promise<void> {
return writeFile(statePath(home), '{"lkg":"corrupted', 'utf8');
return writeFile(statePath(home, STORAGE_KEY), '{"lkg":"corrupted', 'utf8');
}
Loading