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
17 changes: 7 additions & 10 deletions .github/actions/e2e-runtime/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,18 @@ runs:
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: e2e-lab-pnpm-backend-ee-${{ runner.os }}-${{ hashFiles(format('{0}/pnpm-lock.yaml', inputs.teable-ee-path)) }}
key: e2e-lab-pnpm-backend-app-ee-${{ runner.os }}-${{ hashFiles(format('{0}/pnpm-lock.yaml', inputs.teable-ee-path)) }}
restore-keys: |
e2e-lab-pnpm-backend-ee-${{ runner.os }}-

# Only what the run reaches. This job starts one nest app and drives it
# through backend-ee's vitest; the workspace's frontends and their trees
# are installed for nothing, and installing them is not free even on a warm
# store — every package still has to be linked into place. The filter
# covers 43 of the 64 workspace packages, both prisma packages among them,
# and was checked by running the whole suite against it.
e2e-lab-pnpm-backend-app-ee-${{ runner.os }}-

# The backend hosts Vitest and app-ee serves browser-driven cases, so both
# dependency closures are required. The filters retain both Prisma packages
# without linking unrelated workspace applications.
- name: Install teable-ee dependencies
if: inputs.operation == 'bootstrap'
shell: bash
working-directory: ${{ inputs.teable-ee-path }}
run: pnpm install --frozen-lockfile --filter @teable/backend-ee...
run: pnpm install --frozen-lockfile --filter @teable/backend-ee... --filter @teable/app-ee...

- name: Generate Prisma clients
if: inputs.operation == 'bootstrap'
Expand Down
25 changes: 25 additions & 0 deletions cases/authority/y338-unreadable-group-still-loads.case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineBugCase } from "../../framework/types";

export default defineBugCase({
id: "authority/y338-unreadable-group-still-loads",
title:
"Y338: Restricted grouped grids load and keep their permitted behavior",
runner: "authority-unreadable-group",
timeoutMs: 600_000,
bug: {
issue: "T6944",
status: "fixed",
sourceCommits: ["04af0858e"],
},
config: {
baseId: "seed-base",
tableNamePrefix: "e2e-lab-y338",
rows: [
{ title: "alpha", group: "kind-a", status: "open" },
{ title: "beta", group: "kind-b", status: "closed" },
{ title: "gamma", group: "kind-a", status: "open" },
],
subscribeTimeoutMs: 30_000,
settleTimeoutMs: 60_000,
},
});
39 changes: 39 additions & 0 deletions cases/authority/y338-unreadable-group-still-loads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Y338: Restricted grouped grids load and keep their permitted behavior

**T6944 / Y338** — fixed by `04af0858e`.

## What the user sees

A member opens a grid whose persisted group points at a field they cannot read.
The page must load the permitted records, omit that field, and keep its record
subscription healthy instead of surfacing a validation or socket error.

The same degradation applies when the grouping field is conditionally masked.
Existing permissive behavior for a view filter and sort on an unreadable field
must also stay intact.

## What the checkpoint asserts

The browser opens the actual restricted grid. The navigation succeeds, the
grid renders, and the browser's live record subscription omits the unreadable
persisted group, receives every permitted row, and raises no page or socket
error. A separate public v2 request echoes the persisted group and verifies
the same complete, field-filtered record set.

Two API checkpoints then assert that:

1. A persisted group on a conditionally masked field returns only permitted
records instead of rejecting the request.
2. A persisted filter and sort on a statically unreadable field are ignored,
preserving the complete readable record set.

## What the fixture has to hold

The runner creates three deterministic records with title, category, and
status fields; three purpose-built grid views; a real second member; and one
permission role. Owner reads prove all views and rows exist before permissions
are applied. Permission changes between checkpoints happen outside each
checkpoint so a broken fixture cannot be mistaken for T6944.

Extra client-supplied group keys remain strict by design. This case sends only
the persisted group echoed by the grid, which is the input T6944 changed.
19 changes: 19 additions & 0 deletions cases/filter/y339-multi-user-lookup-filter-contract.case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineBugCase } from "../../framework/types";

export default defineBugCase({
id: "filter/y339-multi-user-lookup-filter-contract",
title: "Y339: A multi-user lookup accepts its multi-value filter",
runner: "lookup-user-filter-contract",
timeoutMs: 180_000,
bug: {
issue: "T6943",
status: "fixed",
sourceCommits: ["358787f97"],
},
config: {
baseId: "seed-base",
tableNamePrefix: "e2e-lab-y339",
matchedTitle: "linked-to-current-user",
unmatchedTitle: "not-linked",
},
});
25 changes: 25 additions & 0 deletions cases/filter/y339-multi-user-lookup-filter-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# filter/y339-multi-user-lookup-filter-contract

**T6943 / Y339** — fixed by `358787f97`.

## What the user sees

A one-to-many link looks up a single-value User field. The resulting lookup is
multi-value, so a multi-value user filter must be accepted and return the
matching row.

## What the checkpoint asserts

The public field description marks the lookup as multi-value. Saving a
`hasAnyOf` filter succeeds, and reading the filtered view returns only the row
linked to the selected user.

## What the fixture has to hold

The runner creates a source User field, a host table with matching and
nonmatching rows, a one-to-many link, and the User lookup. It verifies the link
and both unfiltered host rows before the checkpoint.

One representative valid operator is enough to prove the client/server
contract. Exhaustively repeating every multi-value operator is outside this
atomic regression case.
20 changes: 20 additions & 0 deletions cases/lookup/y249-conditional-lookup-keeps-all-matches.case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineBugCase } from "../../framework/types";

export default defineBugCase({
id: "lookup/y249-conditional-lookup-keeps-all-matches",
title: "Y249: A conditional lookup keeps every matching row",
runner: "conditional-lookup-all-matches",
timeoutMs: 180_000,
bug: {
issue: "sentinel/conditional-lookup-all-matches",
status: "fixed",
},
config: {
baseId: "seed-base",
tableNamePrefix: "e2e-lab-y249",
matchKey: 249,
sourceValues: ["match-alpha", "match-beta"],
settleTimeoutMs: 60_000,
pollIntervalMs: 500,
},
});
24 changes: 24 additions & 0 deletions cases/lookup/y249-conditional-lookup-keeps-all-matches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# lookup/y249-conditional-lookup-keeps-all-matches

**Y249 sentinel** — fixed.

## What the user sees

A conditional lookup must not omit records that satisfy its condition. This
case keeps the broad report executable by covering one deterministic contract:
two source rows with the same numeric key both appear in the host row.

## What the checkpoint asserts

The lookup returns exactly both matching source values after computation
settles. It does not use the previously recorded manual result as an oracle.

## What the fixture has to hold

The source table contains two rows with the same numeric key and distinct
values. The host table contains one row with that key. Both tables and the
field-reference condition are created through public APIs before the
checkpoint.

The original report did not identify a confirmed cause or a fix commit, so
this is deliberately a narrow sentinel rather than a historical fix claim.
21 changes: 21 additions & 0 deletions cases/lookup/y340-user-lookup-survives-reread.case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineBugCase } from "../../framework/types";

export default defineBugCase({
id: "lookup/y340-user-lookup-survives-reread",
title: "Y340: A recomputed user lookup survives a fresh read",
runner: "lookup-user-recompute-reread",
timeoutMs: 180_000,
bug: {
issue: "T6941",
status: "fixed",
sourceCommits: ["927f79fa2", "147f587d8"],
},
config: {
baseId: "seed-base",
tableNamePrefix: "e2e-lab-y340",
sourceTitle: "source-owner",
hostTitle: "work-item",
settleTimeoutMs: 60_000,
pollIntervalMs: 500,
},
});
22 changes: 22 additions & 0 deletions cases/lookup/y340-user-lookup-survives-reread.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# lookup/y340-user-lookup-survives-reread

**T6941 / Y340** — fixed by `927f79fa2` and `147f587d8`.

## What the user sees

A User lookup over a one-to-many link shows its computed person. After the
source user changes and the lookup recomputes, refreshing must not make the
value disappear.

## What the checkpoint asserts

Two independent reads first return the original one-item user array. The
runner then changes the source to a different real collaborator and waits for
recomputation. A further independent read returns the replacement user, and
the public field description still marks the lookup as multi-value.

## What the fixture has to hold

The runner creates and invites a second user, then creates the source row, host
row, one-to-many link, and User lookup entirely through public APIs. Before the
checkpoint it verifies that the host row is linked to exactly one source row.
2 changes: 2 additions & 0 deletions e2e-lab.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { INestApplication } from "@nestjs/common";
import { performance } from "node:perf_hooks";
import { initApp } from "../utils/init-app";
import { getBugCase, resolveBugCaseIds } from "./registry";
import { closeBrowserRuntime } from "./framework/browser-runtime";
import { applyEngineRuntimeEnv, LAB_ENGINE } from "./framework/engine";
import { runBugCase } from "./framework/run-bug-case";

Expand Down Expand Up @@ -71,6 +72,7 @@ describe("e2e-lab bug regression runner (e2e)", () => {

afterAll(async () => {
const closeStarted = performance.now();
await closeBrowserRuntime();
await app?.close();
logPhase("app-closed", {
closeMs: Math.round(performance.now() - closeStarted),
Expand Down
Loading
Loading