mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
- Redact caseSettings for non-admins and never apply a shared map on a read-only page (CASE-SYNC-001) - Send one case per save and merge it inside mutateSettingsFile, so devices cannot discard each other's cases; rapid saves keep their order (CASE-SYNC-002) - Check HTTP status and the response body, keep the local choice, report an unsynced save and reuse the forbidden-write handling (CASE-SYNC-003) - Let the server decide whether sync is on: refresh the cached switch, discard stale refreshes (CASE-SYNC-004) - Carry shared options through case rename and drop them on delete or unlink - Give the switch an accessible name; add server, client and two-browser tests Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
88 lines
3.8 KiB
TypeScript
88 lines
3.8 KiB
TypeScript
/**
|
|
* @fileoverview Case sync across two devices, end to end: a real server on an ephemeral port (isolated test
|
|
* HOME) and two separate Chromium contexts (each with its own localStorage, like two devices). Saves of
|
|
* different cases from both contexts both land, the other context takes them on returning to its tab, and
|
|
* once the server says sync is off the selected case stops following.
|
|
*/
|
|
import { mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import { chromium, type Browser, type Page } from 'playwright';
|
|
import { WebServer } from '../src/web/server.js';
|
|
import { getCasesDir } from '../src/config/cases-dir.js';
|
|
|
|
describe('Sync cases across devices in two browsers', () => {
|
|
let server: WebServer;
|
|
let browser: Browser;
|
|
let a: Page;
|
|
let b: Page;
|
|
const one = `sync-a-${process.pid}`;
|
|
const two = `sync-b-${process.pid}`;
|
|
const base = () => `http://localhost:${server.boundPort}`;
|
|
const put = (body: unknown) =>
|
|
fetch(`${base()}/api/settings`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const serverSettings = async () => ((await (await fetch(`${base()}/api/settings`)).json()) as any).data;
|
|
const open = async () => {
|
|
const page = await (await browser.newContext({ viewport: { width: 1400, height: 900 } })).newPage();
|
|
await page.goto(base(), { waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
|
|
return page;
|
|
};
|
|
const opts = (page: Page, name: string) => page.evaluate((n) => (window as any).app.getCaseSettings(n), name);
|
|
const returnToTab = (page: Page) =>
|
|
page.evaluate(async () => {
|
|
const app = (window as any).app;
|
|
await app._syncCaseStateFromServer();
|
|
});
|
|
|
|
beforeAll(async () => {
|
|
for (const name of [one, two]) mkdirSync(join(getCasesDir(), name), { recursive: true });
|
|
server = new WebServer(0, false, true);
|
|
await server.start();
|
|
browser = await chromium.launch({ headless: true });
|
|
await put({ syncCaseState: true });
|
|
a = await open();
|
|
b = await open();
|
|
for (const page of [a, b]) {
|
|
await page.evaluate(async () => {
|
|
const app = (window as any).app;
|
|
await app.loadAppSettingsFromServer();
|
|
});
|
|
}
|
|
}, 60000);
|
|
|
|
afterAll(async () => {
|
|
await browser?.close();
|
|
await server?.stop();
|
|
}, 60000);
|
|
|
|
it("keeps both devices' saves and lets the other device take them", async () => {
|
|
await Promise.all([
|
|
a.evaluate((n) => (window as any).app.saveCaseSettings(n, { agentTeams: true, opusContext1m: false }), one),
|
|
b.evaluate((n) => (window as any).app.saveCaseSettings(n, { agentTeams: false, opusContext1m: false }), two),
|
|
]);
|
|
await expect
|
|
.poll(async () => Object.keys((await serverSettings()).caseSettings ?? {}).sort(), { timeout: 10000 })
|
|
.toEqual([one, two].sort());
|
|
await returnToTab(b);
|
|
await returnToTab(a);
|
|
expect(await opts(b, one)).toEqual({ agentTeams: true, opusContext1m: false });
|
|
expect(await opts(a, two)).toEqual({ agentTeams: false, opusContext1m: false });
|
|
});
|
|
|
|
it('follows the selected case while sync is on, and stops once the server says it is off', async () => {
|
|
await put({ lastUsedCase: two });
|
|
await returnToTab(a);
|
|
expect(await a.evaluate(() => (document.getElementById('quickStartCase') as HTMLSelectElement).value)).toBe(two);
|
|
|
|
await put({ syncCaseState: false, lastUsedCase: one });
|
|
await returnToTab(a);
|
|
expect(await a.evaluate(() => (document.getElementById('quickStartCase') as HTMLSelectElement).value)).toBe(two);
|
|
expect(await a.evaluate(() => (window as any).app.loadAppSettingsFromStorage().syncCaseState)).toBe(false);
|
|
});
|
|
});
|