Skip to content
Closed
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: 1 addition & 1 deletion api/stovepipe/proto/stovepipe.proto
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ message GetProjectStatusByURIRequest {
string queue = 1;
// Exact VCS-agnostic URI of the commit under validation.
string change_uri = 2;
// Required consumer-defined project IDs whose recorded results are requested.
// Consumer-defined project IDs whose recorded results are requested. Empty returns request and repository status only.
repeated string projects = 3;
// Maximum requested projects to return in one page. Zero selects the server default.
int32 page_size = 4;
Expand Down
2 changes: 1 addition & 1 deletion api/stovepipe/protopb/stovepipe.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions stovepipe/controller/get_project_status_by_uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,6 @@ func validateProjectStatusRequest(req entity.GetProjectStatusByURIRequest) error
if err := validateHistoryIdentifier("change URI", req.ChangeURI); err != nil {
return fmt.Errorf("GetProjectStatusByURI invalid change_uri=%q: %w", req.ChangeURI, err)
}
if len(req.Projects) == 0 {
return fmt.Errorf("GetProjectStatusByURI projects must be non-empty: %w", ErrInvalidRequest)
}
seen := make(map[string]struct{}, len(req.Projects))
for _, project := range req.Projects {
if err := validateHistoryIdentifier("project", project); err != nil {
Expand Down
6 changes: 5 additions & 1 deletion stovepipe/controller/get_project_status_by_uri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ func TestGetProjectStatusByURI(t *testing.T) {
{name: "missing uri", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}, uriErr: storage.ErrNotFound, wantNotFound: true},
{name: "request summary visibility gap", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}, summaryErr: storage.ErrNotFound, wantRetryable: true},
{name: "empty queue", request: entity.GetProjectStatusByURIRequest{ChangeURI: projectStatusURI}, wantInvalid: true},
{name: "empty projects", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI}, wantInvalid: true},
{name: "no projects without repository fact", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI}, factErr: storage.ErrNotFound},
{name: "empty projects with repository fact", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{}}, fact: entity.ValidationFact{URI: projectStatusURI, RequestID: projectStatusID, Degree: entity.DegreeGreen}, wantFact: true},
{name: "project page token without projects", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, PageToken: "token"}, factErr: storage.ErrNotFound, wantInvalid: true, noProjectLookup: true},
{name: "empty project", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{""}}, wantInvalid: true},
{name: "duplicate projects", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a", "project-a"}}, wantInvalid: true},
{name: "invalid page size", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, PageSize: maxProjectStatusPageSize + 1}, wantInvalid: true},
Expand Down Expand Up @@ -110,6 +112,8 @@ func TestGetProjectStatusByURI(t *testing.T) {
assert.Equal(t, requestSummary, got.RequestSummary)
assert.Equal(t, tt.wantFact, got.HasRepositoryValidationFact)
assert.False(t, got.ProjectResultsComplete)
assert.Empty(t, got.ProjectValidationFacts)
assert.Empty(t, got.NextPageToken)
if tt.wantFact {
assert.Equal(t, tt.fact, got.RepositoryValidationFact)
}
Expand Down
2 changes: 1 addition & 1 deletion stovepipe/entity/project_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type GetProjectStatusByURIRequest struct {
Queue string
// ChangeURI identifies the exact commit under validation.
ChangeURI string
// Projects limits results to the supplied consumer-defined project IDs.
// Projects limits results to the supplied consumer-defined project IDs; empty returns request and repository status only.
Projects []string
// PageSize is the requested maximum number of requested projects to inspect.
PageSize int32
Expand Down
2 changes: 1 addition & 1 deletion web/api/src/gen/api/stovepipe/proto/stovepipe_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ export type GetProjectStatusByURIRequest = Message<"uber.submitqueue.stovepipe.G
changeUri: string;

/**
* Required consumer-defined project IDs whose recorded results are requested.
* Consumer-defined project IDs whose recorded results are requested. Empty returns request and repository status only.
*
* @generated from field: repeated string projects = 3;
*/
Expand Down
2 changes: 1 addition & 1 deletion web/service/stovepipe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ For a separate backend or development server, configure the environment below an
| `STOVEPIPE_URL` | Backend origin; the local overlay sets `http://stovepipe-service:8080`. |
| `STOVEPIPE_ALLOW_PLAINTEXT` | Set `true` for local HTTP/2 without TLS. |
| `STOVEPIPE_WEB_TOKEN` | Required Basic authentication password; the local overlay defaults to `test`. |
| `STOVEPIPE_WEB_QUEUES` | JSON array of queue objects with `name` and nonempty `projects` arrays. Names must match the backend; project IDs must be unique within each queue. The local overlay supplies the demo queues. |
| `STOVEPIPE_WEB_QUEUES` | JSON array of queue objects with `name` and `projects` arrays. Use an empty array to load request status, repository results, and history without project results. Names must match the backend; project IDs must be unique within each queue. The local overlay supplies the demo queues with empty project arrays. |
| `LOG_LEVEL` | JSON logging level; defaults to `info`. |

A queue such as `monorepo/main` appears at `/monorepo%2Fmain`; its change page appears at `/monorepo%2Fmain/change/<percent-escaped-uri>`.
Expand Down
2 changes: 1 addition & 1 deletion web/service/stovepipe/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
STOVEPIPE_URL: http://stovepipe-service:8080
STOVEPIPE_ALLOW_PLAINTEXT: "true"
STOVEPIPE_WEB_TOKEN: ${STOVEPIPE_WEB_TOKEN:-test}
STOVEPIPE_WEB_QUEUES: ${STOVEPIPE_WEB_QUEUES:-[{"name":"monorepo/main","projects":["example"]},{"name":"monorepo/release","projects":["example"]},{"name":"monorepo/slow","projects":["example"]}]}
STOVEPIPE_WEB_QUEUES: ${STOVEPIPE_WEB_QUEUES:-[{"name":"monorepo/main","projects":[]},{"name":"monorepo/release","projects":[]},{"name":"monorepo/slow","projects":[]}]}
depends_on:
stovepipe-service:
condition: service_started
Expand Down
5 changes: 3 additions & 2 deletions web/service/stovepipe/src/server/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe("Stovepipe host configuration", () => {
it("preserves queue and project identities", () => {
const queues = [
{ name: "repo/main", projects: ["project/a"] },
{ name: "repo/empty", projects: ["project/b"] },
{ name: "repo/empty", projects: [] },
];
expect(
loadConfiguration({
Expand All @@ -20,7 +20,8 @@ describe("Stovepipe host configuration", () => {
it("rejects missing or ambiguous queue/project configuration", () => {
for (const queues of [
[],
[{ name: "q", projects: [] }],
[{ name: "q" }],
[{ name: "q", projects: [""] }],
[{ name: "q", projects: ["a", "a"] }],
[
{ name: "q", projects: ["a"] },
Expand Down
3 changes: 1 addition & 2 deletions web/service/stovepipe/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ export function loadConfiguration(
typeof queue.name !== "string" ||
!queue.name ||
!Array.isArray(queue.projects) ||
queue.projects.length === 0 ||
queue.projects.some(
(project: unknown) => typeof project !== "string" || !project,
)
)
throw new Error("Each queue needs a name and at least one project ID");
throw new Error("Each queue needs a name and an array of project IDs");
if (
queues.some((item) => item.name === queue.name) ||
new Set(queue.projects).size !== queue.projects.length
Expand Down
2 changes: 1 addition & 1 deletion web/stovepipe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Use the server entry's `createStovepipeWeb` to load a page and render its model

Change pages use `/<queue>/change/<percent-escaped-uri>` and look up status with `GetProjectStatusByURI`. History uses the request ID returned by that lookup. The `/request/` path is reserved for a future ID-based view. Queue pagination uses the backend's opaque continuations, and the latest URL carries no time window.

Project results request ten configured project IDs per page beside the request status, with history below. Only entries returned by the backend are displayed; a page can have fewer than ten results or be empty. The raw JSON link collects the returned project entries across all backend pages, independent of display pagination. The reference host serializes only the projects array as protobuf JSON. Configured projects are not an inventory of every project in a repository.
Project results request ten configured project IDs per page beside the request status, with history below. With an empty project list, `GetProjectStatusByURI` still returns request and repository status, and history loads using the returned request ID. Only entries returned by the backend are displayed; a page can have fewer than ten results or be empty. The raw JSON link collects the returned project entries across all backend pages, independent of display pagination. The reference host serializes only the projects array as protobuf JSON. Configured projects are not an inventory of every project in a repository.

## Refresh and presentation

Expand Down
2 changes: 1 addition & 1 deletion web/stovepipe/entity/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export interface Queue {
/** Queue name, preserved verbatim in the UI. */
name: string;
/** Nonempty consumer-selected project IDs. */
/** Consumer-selected project IDs; empty requests only request and repository status. */
projects: readonly string[];
}
export type ReadResult<T> =
Expand Down
37 changes: 37 additions & 0 deletions web/stovepipe/module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,43 @@ describe("Stovepipe page loading", () => {
});
expect(() => JSON.stringify(result)).not.toThrow();
});
it("loads status and history without project selectors", async () => {
const { app, service, paths } = createPageTestContext([]);
vi.mocked(service.getProjectStatusByURI).mockResolvedValue({
...status,
projects: [],
});

const result = await app.handle({
path: paths.change("repo/main", changeUri),
search: new URLSearchParams(),
});

expect(service.getProjectStatusByURI).toHaveBeenCalledWith(
{ queue: "repo/main", changeUri, projects: [], pageSize: 10, pageToken: "" },
{},
);
expect(result).toMatchObject({
kind: "render",
model: {
requestId: "7",
status: {
ok: true,
data: { repositoryDegree: 0, projects: [], nextHref: null },
},
history: { ok: true },
},
});
expect(service.getRequestHistoryByID).toHaveBeenCalledWith(
{ queue: "repo/main", requestId: "7" },
{},
);

expect(await app.handle({
path: paths.projectJSON("repo/main", changeUri),
search: new URLSearchParams(),
})).toEqual({ kind: "project-json", data: [] });
});
it("requests ten configured projects per page without adding unreturned projects", async () => {
const projects = Array.from(
{ length: 11 },
Expand Down
4 changes: 2 additions & 2 deletions web/test/e2e/stovepipe/run_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ def main():
)
env["STOVEPIPE_WEB_QUEUES"] = json.dumps(
[
{"name": "monorepo/main", "projects": ["example"]},
{"name": "monorepo/main", "projects": []},
{
"name": "web/pagination",
"projects": [f"project-{i}" for i in range(1, 52)],
},
{"name": "web/empty", "projects": ["example"]},
{"name": "web/empty", "projects": []},
]
)
compose = [
Expand Down
4 changes: 4 additions & 0 deletions web/test/e2e/stovepipe/stovepipe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ test("real ingestion, queue selection, cursors, project results, history and ref
await expect(
page.getByText("No project results recorded on this page."),
).toBeVisible();
const rawHref = await rawProjects.getAttribute("href");
const response = await page.request.get(rawHref!);
expect(response.ok()).toBe(true);
expect(await response.json()).toEqual([]);
await refresh.click();
await expect(refresh).toBeEnabled();
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
Expand Down