Files
improvise/harness/tests/drill.spec.js
T
Edward Langley f7d2214c7a test(harness): drill is refused in the browser, and one refusal is silent
The fourth widget reported, and the one with a different answer: there is no
wire vocabulary for drill. reduce.rs contains no Drill variant at all, neither
Command nor ModelCommand. That is improvise-gd28, and it is a decision rather
than an oversight — drilling writes drill_state, which gotchas 30 and 1 rule
session-local, and a Command::Model is routed with no SessionId. Adding
ModelCommand::Drill would reintroduce exactly what improvise-3x3u removed.

Everything below the command works, verified in the browser: CellId::Aggregate
is emitted and received, editable answers false for it, and edit_or_drill
reaches the aggregate arm. The TUI is the control and passes 12 drill tests, so
this is isolated to the client/wire.

The finding gd28 did not record is that the two entry points fail DIFFERENTLY.
`i`/`a` declines out loud with a status. `>` — the key the hint line actually
advertises as ">:drill" — is a name resolve::step does not handle, so it goes
KeyOutcome::Upstream -> ClientFrame::Key -> Host::handle's empty vec: a black
hole, not a slow path (gotcha 47). Silent. That asymmetry is why this reads as
broken rather than refused.

These tests document what is true rather than leaving folklore. They MAKE an
aggregate through the tile bar (web/demo.improv has none by construction —
gotcha 46) and assert the precondition, so they cannot pass against a grid of
Records. Both proved by injection: silencing the aggregate arm fails both, the
second on its check_the_checker control; making `>` resolve fails only the
second.

Evidence added to improvise-gd28 rather than filing a duplicate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:14:53 -07:00

77 lines
3.6 KiB
JavaScript

// Drill in the browser: what it does today, which is refuse. improvise-gd28
// says why, and context/test-evidence/clients.md records the layer it stops at.
import { test, expect } from "@playwright/test";
/// `web/demo.improv` has NO aggregate cell by construction — gotcha 46 puts
/// exactly one category on each visible axis so every cell is a Record, and
/// `tests/web_demo_model.rs` asserts it. So a drill test must MAKE one first.
async function anAggregateGrid(page) {
await page.goto("/app/index.html");
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
await page.locator("#grid").focus();
await expect(page.locator("#grid td.editable").first()).toBeVisible();
// Quarter is on Page; one cycle takes it to None, so every cell aggregates
// over the four quarters. The tile bar is how a user would do it.
await page.keyboard.press("Shift+T");
const tiles = page.locator("#tiles .tile");
const quarter = tiles.filter({ hasText: "Quarter" });
await expect(quarter).toHaveText("Quarter Pag");
const names = await tiles.allTextContents();
for (let i = 0; i < names.findIndex((t) => t.startsWith("Quarter")); i++) {
await page.keyboard.press("ArrowRight");
}
await expect(quarter).toHaveClass(/selected/);
await page.keyboard.press("Enter");
await expect(quarter).toHaveText("Quarter ·", { timeout: 10_000 });
await page.keyboard.press("Escape");
// The precondition, asserted rather than assumed: nothing is editable now,
// which is `CellId::Aggregate` reaching `render.rs::editable` (ADR 002).
await expect(page.locator("#grid td.editable")).toHaveCount(0);
await expect(page.locator("#grid tbody td").first()).toBeVisible();
}
test.describe("drill is refused in the browser, not broken", () => {
// The honest half: `i`/`a` on an aggregate declines OUT LOUD, because
// `edit_or_drill` answers the `CellId::Aggregate` arm with a status.
test("editing an aggregate says drill needs the host", async ({ page }) => {
await anAggregateGrid(page);
await page.keyboard.press("i");
await expect(page.locator("#status")).toContainText(
"drilling into an aggregate needs the host",
);
// And it really is a refusal: no editor opened, no mode change.
await expect(page.locator("#mode")).toHaveText("NORMAL");
await expect(page.locator("#grid input")).toHaveCount(0);
});
// The silent half, and the one worth pinning. `>` is `drill-into-cell`,
// which `resolve::step` does not name, so it becomes `KeyOutcome::Upstream`
// -> `ClientFrame::Key` -> `Host::handle`, whose Key arm is an empty vec
// (gotcha 47). An upstream key is a black hole, not a slow path.
test("the drill key does nothing at all, and the page is not merely dead", async ({
page,
}) => {
await anAggregateGrid(page);
const before = await page.locator("#status").textContent();
await page.keyboard.press(">");
// Nothing moves: not the mode, not the status, not the grid.
await expect(page.locator("#mode")).toHaveText("NORMAL");
await expect(page.locator("#status")).toHaveText(before);
await expect(page.locator("#grid td.editable")).toHaveCount(0);
// check_the_checker: a test that asserts "nothing happened" passes against
// a page that has stopped listening. This proves the keyboard is live, so
// `>`'s silence is specific to `>` — the refusal below DOES change #status.
await page.keyboard.press("i");
await expect(page.locator("#status")).not.toHaveText(before);
await expect(page.locator("#status")).toContainText(
"drilling into an aggregate needs the host",
);
});
});