Skip to content
Open
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
2 changes: 2 additions & 0 deletions .changeset/cyan-taxes-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
68 changes: 23 additions & 45 deletions src/ui/AppHost.workspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ async function flushUntil(
}
}

/** Require both review chrome and row content so lazy Git source reads have completed. */
function hasRenderedAlphaReview(setup: Awaited<ReturnType<typeof testRender>>) {
const frame = setup.captureCharFrame();
return frame.includes("alpha.txt") && frame.includes("one");
}

/**
* Write the fixture whose `y` command probes the affordance and then asks for a
* whole-document replacement, logging both answers.
Expand Down Expand Up @@ -239,11 +245,7 @@ describe("extension workspace reads", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");

await act(async () => {
await setup.mockInput.typeText("y");
Expand Down Expand Up @@ -277,11 +279,7 @@ describe("extension workspace reads", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");

await act(async () => {
await setup.mockInput.typeText("y");
Expand Down Expand Up @@ -315,11 +313,7 @@ describe("extension workspace reads", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");

await act(async () => {
await setup.mockInput.typeText("y");
Expand Down Expand Up @@ -348,11 +342,7 @@ describe("extension workspace reads", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");

await act(async () => {
await setup.mockInput.typeText("y");
Expand All @@ -374,6 +364,14 @@ describe("extension workspace reads", () => {
// The written text is the read text transformed, so the read reached the
// whole document rather than the patch the review was built from.
expect(readFileSync(join(repo, "alpha.txt"), "utf8")).toBe("ONE\nTWO\n");

// The successful write starts a review reload. Wait for it to finish before teardown so a
// Git child process cannot retain the temporary checkout on Windows.
await flushUntil(
setup,
() => setup.captureCharFrame().includes("ONE"),
"the reloaded review to show the transformed content",
);
});
});
});
Expand All @@ -393,11 +391,7 @@ describe("extension workspace writes", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");

await act(async () => {
await setup.mockInput.typeText("y");
Expand Down Expand Up @@ -451,11 +445,7 @@ describe("extension workspace writes", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");
await act(async () => {
await setup.mockInput.typeText("y");
});
Expand Down Expand Up @@ -499,11 +489,7 @@ describe("extension workspace writes", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");
await act(async () => {
await setup.mockInput.typeText("y");
});
Expand Down Expand Up @@ -543,11 +529,7 @@ describe("extension workspace writes", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");

await act(async () => {
await setup.mockInput.typeText("y");
Expand Down Expand Up @@ -628,11 +610,7 @@ describe("extension workspace writes", () => {
options: { mode: "stack", extensionPaths: [extPath] },
});
await withAppHost(bootstrap, async (setup) => {
await flushUntil(
setup,
() => setup.captureCharFrame().includes("alpha.txt"),
"the review to render",
);
await flushUntil(setup, () => hasRenderedAlphaReview(setup), "the review to render");

await act(async () => {
await setup.mockInput.typeText("y");
Expand Down
3 changes: 3 additions & 0 deletions test/pty/chrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ describe("PTY chrome", () => {
expect(initial).toContain("add = true");
expect(initial).toContain("betaValue");

// First paint can precede keyboard subscription under CI load; prove input is live first.
await harness.ensureKeyboardIsLive(session);
await session.press("tab");
await session.type("beta");
const filtered = await harness.waitForSnapshot(
Expand Down Expand Up @@ -178,6 +180,7 @@ describe("PTY chrome", () => {
expect(initial).toContain("alphaOnly = true");
expect(initial).toContain("betaValue = 2");

await harness.ensureKeyboardIsLive(session);
await session.type("/");
await harness.waitForSnapshot(
session,
Expand Down
1 change: 1 addition & 0 deletions test/pty/filter-escape.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe("filter escape clearing (PTY)", () => {

try {
await session.waitForText(/View\s+Navigate\s+Agent\s+Help/, { timeout: 15_000 });
await harness.ensureKeyboardIsLive(session);

// Open filter, type a no-match query.
await session.type("/");
Expand Down
21 changes: 14 additions & 7 deletions test/pty/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,13 +902,16 @@ end
let snapshot = await session.text({ immediate: true });

while (Date.now() - start < timeoutMs) {
if (predicate(snapshot)) {
return snapshot;
}

// A resize can make structural markers visible before their row text finishes painting.
// Only evaluate frames after the PTY has had a quiet period instead of accepting that
// transitional first capture.
await session.waitIdle({ timeout: 50 });
await sleep(30);
snapshot = await session.text({ immediate: true });

if (predicate(snapshot)) {
return snapshot;
}
}

throw new Error(
Expand All @@ -935,12 +938,16 @@ end
await session.press("?");
try {
await waitForSnapshot(session, (text) => text.includes("Controls help"), 2_000);
await session.press("escape");
await waitForSnapshot(session, (text) => !text.includes("Controls help"), 5_000);
return;
} catch {
// Dropped before the app was listening; the next press is the retry.
continue;
}

// Once the probe opens, closing it is part of the test contract. Surface a dropped Escape
// rather than sending another probe that could hide which transition failed.
await session.press("escape");
await waitForSnapshot(session, (text) => !text.includes("Controls help"), 5_000);
return;
}

throw new Error("The app never reacted to a keypress.");
Expand Down
6 changes: 5 additions & 1 deletion test/pty/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,11 @@ describe("PTY layout", () => {
session.resize({ cols: 140, rows: 24 });
const tight = await harness.waitForSnapshot(
session,
(text) => /▌.*▌/.test(text) && harness.countMatches(text, /alpha\.ts/g) === 1,
// Structural resize markers can appear before the row text finishes painting.
(text) =>
/▌.*▌/.test(text) &&
harness.countMatches(text, /alpha\.ts/g) === 1 &&
text.includes("betaValue = 1"),
5_000,
);

Expand Down
Loading