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
30 changes: 11 additions & 19 deletions e2e/agent-review.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from "node:path";
import {
closeElectronApp,
ensureFilesViewOpenInLeftPanel,
expectWorkspaceSessionOpenFilePaths,
fileTreeFile,
launchDevElectronAppWithArgs,
registerRendererConsoleLogging,
Expand All @@ -20,6 +21,7 @@ test("Atelier reveals a review after Codex edits restored markdown", async ({
const workspaceDir = testInfo.outputPath("workspace");
const welcomeFilePath = path.join(workspaceDir, "welcome.md");
const changelogFilePath = path.join(workspaceDir, "changelog.md");
const fallbackFilePath = path.join(workspaceDir, "fallback.md");
const createdFilePath = path.join(workspaceDir, "codex-created.md");
const fakeBinDir = testInfo.outputPath("fake-bin");
const fakeCodexCompletionPath = testInfo.outputPath("fake-codex-complete");
Expand Down Expand Up @@ -49,10 +51,15 @@ test("Atelier reveals a review after Codex edits restored markdown", async ({
registerRendererConsoleLogging(page);

await openWelcomeMarkdown(page);
await expectOpenFilePersisted(page, "/welcome.md");
await expectWorkspaceSessionOpenFilePaths(userDataDir, workspaceDir, [
"welcome.md",
]);

await closeElectronApp(electronApp);
electronApp = undefined;
const fallbackMarkdownTime = new Date(newestMarkdownTime.getTime() + 1_000);
await writeFile(fallbackFilePath, "# Fallback\n", "utf8");
await utimes(fallbackFilePath, fallbackMarkdownTime, fallbackMarkdownTime);

electronApp = await launchDevElectronAppWithArgs([], { userDataDir });
page = await electronApp.firstWindow();
Expand Down Expand Up @@ -84,7 +91,9 @@ test("Atelier reveals a review after Codex edits restored markdown", async ({
.toContain("Codex created file");

await expect(
page.getByRole("group", { name: /^Review change 1 of \d+$/ }),
page.getByRole("group", {
name: /^Review change 1 of \d+(?:, \d+ remaining)?$/,
}),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Keep change" }),
Expand Down Expand Up @@ -129,23 +138,6 @@ async function openWelcomeMarkdown(page: Page): Promise<void> {
).toBeVisible();
}

async function expectOpenFilePersisted(
page: Page,
filePath: string,
): Promise<void> {
await expect
.poll(async () => {
return await page.evaluate(async (key) => {
const result = await window.flashtypeDesktop?.lix.execute({
sql: "SELECT value FROM lix_key_value_by_branch WHERE key = $1 AND lixcol_branch_id = 'global'",
params: [key],
});
return JSON.stringify(result?.rows?.[0]?.[0] ?? null);
}, "atelier_ui_state");
})
.toContain(filePath);
}

async function writeFakeCodex(binDir: string): Promise<void> {
await mkdir(binDir, { recursive: true });
const scriptPath = path.join(binDir, "codex");
Expand Down
258 changes: 94 additions & 164 deletions e2e/branch-switcher.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test, type Page } from "@playwright/test";
import type { ElectronApplication } from "playwright";
import type { AtelierSessionUiState } from "@opral/atelier";
import { LocalFilesystem, openLix } from "@lix-js/sdk";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
Expand Down Expand Up @@ -871,6 +872,21 @@ type ActiveCentralDocumentIdentity = {
readonly filePath: string;
};

async function atelierSessionStateFromUi(
page: Page,
): Promise<AtelierSessionUiState | null> {
return await page.evaluate(() => {
const e2e = (
window as Window & {
__flashtypeE2E?: {
getAtelierSessionState: () => AtelierSessionUiState | null;
};
}
).__flashtypeE2E;
return e2e?.getAtelierSessionState() ?? null;
});
}

async function expectActiveCentralDocumentIdentityForPath(
page: Page,
appPath: string,
Expand Down Expand Up @@ -904,73 +920,34 @@ async function expectActiveCentralDocumentIdentity(
async function activeCentralDocumentIdentityFromUi(
page: Page,
): Promise<ActiveCentralDocumentIdentity | null> {
return await page.evaluate(async () => {
const result = await window.flashtypeDesktop?.lix.execute({
sql: "SELECT value FROM lix_key_value_by_branch WHERE key = $1 AND lixcol_branch_id = $2",
params: ["atelier_ui_state", "global"],
});
const state = result?.rows?.[0]?.[0] as
| {
panels?: {
central?: {
activeInstance?: string | null;
views?: Array<{
instance?: unknown;
kind?: unknown;
state?: { fileId?: unknown; filePath?: unknown };
}>;
};
};
}
| undefined;
const central = state?.panels?.central;
const views = central?.views ?? [];
const active =
views.find((view) => view.instance === central?.activeInstance) ??
views[0];
const instance = active?.instance;
const kind = active?.kind;
const fileId = active?.state?.fileId;
const filePath = active?.state?.filePath;
if (
typeof instance !== "string" ||
typeof kind !== "string" ||
typeof fileId !== "string" ||
typeof filePath !== "string"
) {
return null;
}
return { instance, kind, fileId, filePath };
});
const state = await atelierSessionStateFromUi(page);
const central = state?.panels.central;
const views = central?.views ?? [];
const active =
views.find((view) => view.instance === central?.activeInstance) ?? views[0];
const instance = active?.instance;
const kind = active?.kind;
const fileId = active?.state?.fileId;
const filePath = active?.state?.filePath;
if (
typeof instance !== "string" ||
typeof kind !== "string" ||
typeof fileId !== "string" ||
typeof filePath !== "string"
) {
return null;
}
return { instance, kind, fileId, filePath };
}

async function activeCentralFilePathFromUi(page: Page): Promise<string | null> {
return await page.evaluate(async () => {
const result = await window.flashtypeDesktop?.lix.execute({
sql: "SELECT value FROM lix_key_value_by_branch WHERE key = $1 AND lixcol_branch_id = $2",
params: ["atelier_ui_state", "global"],
});
const state = result?.rows?.[0]?.[0] as
| {
panels?: {
central?: {
activeInstance?: string | null;
views?: Array<{
instance?: string;
state?: { filePath?: unknown };
}>;
};
};
}
| undefined;
const central = state?.panels?.central;
const views = central?.views ?? [];
const active =
views.find((view) => view.instance === central?.activeInstance) ??
views[0];
const filePath = active?.state?.filePath;
return typeof filePath === "string" ? filePath : null;
});
const state = await atelierSessionStateFromUi(page);
const central = state?.panels.central;
const views = central?.views ?? [];
const active =
views.find((view) => view.instance === central?.activeInstance) ?? views[0];
const filePath = active?.state?.filePath;
return typeof filePath === "string" ? filePath : null;
}

async function expectActiveEditorRevisionState(
Expand All @@ -996,113 +973,66 @@ async function expectSingleCentralDocumentSlot(page: Page): Promise<void> {
}

async function documentSlotViolationsFromUi(page: Page): Promise<string[]> {
return await page.evaluate(async () => {
const result = await window.flashtypeDesktop?.lix.execute({
sql: "SELECT value FROM lix_key_value_by_branch WHERE key = $1 AND lixcol_branch_id = $2",
params: ["atelier_ui_state", "global"],
});
type ViewState = {
readonly fileId?: unknown;
};
type View = {
readonly instance?: unknown;
readonly kind?: unknown;
readonly state?: ViewState;
};
type Panel = {
readonly activeInstance?: unknown;
readonly views?: View[];
};
const state = result?.rows?.[0]?.[0] as
| {
panels?: {
left?: Panel;
central?: Panel;
right?: Panel;
};
}
| undefined;
const panels = state?.panels ?? {};
const violations: string[] = [];
const isDocumentView = (view: View): boolean => {
const fileId = view.state?.fileId;
return (
typeof view.kind === "string" &&
typeof view.instance === "string" &&
typeof fileId === "string" &&
view.instance === `${view.kind}:${fileId}`
);
};
for (const side of ["left", "right"] as const) {
const documentViews = panels[side]?.views?.filter(isDocumentView) ?? [];
if (documentViews.length > 0) {
violations.push(`${side} has ${documentViews.length} document view(s)`);
}
}
const central = panels.central;
const centralViews = central?.views ?? [];
if (centralViews.length > 1) {
violations.push(`central has ${centralViews.length} views`);
}
const centralView = centralViews[0];
if (centralView && !isDocumentView(centralView)) {
violations.push("central view is not a document view");
}
if (
centralView &&
typeof centralView.instance === "string" &&
central?.activeInstance !== centralView.instance
) {
violations.push("central document view is not active");
const panels = (await atelierSessionStateFromUi(page))?.panels;
const violations: string[] = [];
const isDocumentView = (
view: AtelierSessionUiState["panels"]["central"]["views"][number],
): boolean => {
const fileId = view.state?.fileId;
return (
typeof view.kind === "string" &&
typeof view.instance === "string" &&
typeof fileId === "string" &&
view.instance === `${view.kind}:${fileId}`
);
};
for (const side of ["left", "right"] as const) {
const documentViews = panels?.[side].views.filter(isDocumentView) ?? [];
if (documentViews.length > 0) {
violations.push(`${side} has ${documentViews.length} document view(s)`);
}
return violations;
});
}
const central = panels?.central;
const centralViews = central?.views ?? [];
if (centralViews.length > 1) {
violations.push(`central has ${centralViews.length} views`);
}
const centralView = centralViews[0];
if (centralView && !isDocumentView(centralView)) {
violations.push("central view is not a document view");
}
if (
centralView &&
typeof centralView.instance === "string" &&
central?.activeInstance !== centralView.instance
) {
violations.push("central document view is not active");
}
return violations;
}

async function activeEditorRevisionStateFromUi(page: Page): Promise<{
beforeCommitId: string | null;
afterCommitId: string | null;
} | null> {
return await page.evaluate(async () => {
const result = await window.flashtypeDesktop?.lix.execute({
sql: "SELECT value FROM lix_key_value_by_branch WHERE key = $1 AND lixcol_branch_id = $2",
params: ["atelier_ui_state", "global"],
});
const state = result?.rows?.[0]?.[0] as
| {
panels?: {
central?: {
activeInstance?: string | null;
views?: Array<{
instance?: string;
state?: {
beforeCommitId?: unknown;
afterCommitId?: unknown;
};
}>;
};
};
}
| undefined;
const central = state?.panels?.central;
const views = central?.views ?? [];
const active =
views.find((view) => view.instance === central?.activeInstance) ??
views[0];
if (!active) return null;
const beforeCommitId = active.state?.beforeCommitId;
const afterCommitId = active.state?.afterCommitId;
return {
beforeCommitId:
typeof beforeCommitId === "string" && beforeCommitId.length > 0
? beforeCommitId
: null,
afterCommitId:
typeof afterCommitId === "string" && afterCommitId.length > 0
? afterCommitId
: null,
};
});
const state = await atelierSessionStateFromUi(page);
const central = state?.panels.central;
const views = central?.views ?? [];
const active =
views.find((view) => view.instance === central?.activeInstance) ?? views[0];
if (!active) return null;
const beforeCommitId = active.state?.beforeCommitId;
const afterCommitId = active.state?.afterCommitId;
return {
beforeCommitId:
typeof beforeCommitId === "string" && beforeCommitId.length > 0
? beforeCommitId
: null,
afterCommitId:
typeof afterCommitId === "string" && afterCommitId.length > 0
? afterCommitId
: null,
};
}

async function expectFileTreeStatuses(
Expand Down
25 changes: 25 additions & 0 deletions e2e/electron-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,31 @@ export async function expectPathMissing(filePath: string): Promise<void> {
.toBe(true);
}

export async function expectWorkspaceSessionOpenFilePaths(
userDataDir: string,
workspacePath: string,
openFilePaths: string[],
): Promise<void> {
await expect
.poll(async () => {
try {
const store = JSON.parse(
await readFile(
path.join(userDataDir, "workspace-session.json"),
"utf8",
),
);
const workspace = Array.isArray(store.workspaces)
? store.workspaces.find((entry: any) => entry.path === workspacePath)
: undefined;
return workspace?.openFilePaths ?? null;
} catch {
return null;
}
})
.toEqual(openFilePaths);
}

export async function closeElectronApp(
electronApp: ElectronApplication | undefined,
): Promise<void> {
Expand Down
Loading
Loading