Skip to content

Commit 3b55e71

Browse files
committed
Fix screenshot streaming and E2E verification
1 parent 981791a commit 3b55e71

File tree

7 files changed

+310
-25
lines changed

7 files changed

+310
-25
lines changed

.claude-skills/e2e-testing_skill/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ bun run test:e2e:ui
3838
- Total Playwright test time per spec ≤ 60 seconds (budget defined in references)
3939
- Failures include screenshot + console output attachments
4040
- Regressions traced back to backend/frontend root cause with notes in Graphiti
41+
- Run-flow assertions confirm both timeline events and visible screenshot gallery output
4142

4243
## Reference Library
4344
- `references/automated-tests.md` – Quick start commands, configuration, timeout policy, and pre-push integration

backend/artifacts/dto.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,13 @@ export interface GetArtifactMetaResponse {
4141
contentHashSha256: string | null;
4242
createdAt: string;
4343
}
44+
45+
/**
46+
* GetArtifactContentResponse includes inline artifact data for rendering in clients.
47+
* PURPOSE: Provide encoded screenshot content to frontend consumers without exposing buckets directly.
48+
*/
49+
export interface GetArtifactContentResponse {
50+
refId: string;
51+
mimeType: string;
52+
dataUrl: string;
53+
}

backend/artifacts/get-content.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { APIError, type Query, api } from "encore.dev/api";
2+
import log from "encore.dev/log";
3+
import { artifactsBucket } from "./bucket";
4+
import type { GetArtifactContentResponse } from "./dto";
5+
6+
/**
7+
* getArtifactContent returns inline screenshot data for a given artifact refId.
8+
* PURPOSE: Enable frontend clients to render screenshots without direct bucket access.
9+
*/
10+
export const getArtifactContent = api<{ refId?: Query<string> }, GetArtifactContentResponse>(
11+
{ expose: true, method: "GET", path: "/artifacts/content" },
12+
async ({ refId }) => {
13+
const logger = log.with({ module: "artifacts", actor: "getContent", refId });
14+
15+
if (!refId) {
16+
logger.warn("Missing refId query parameter");
17+
throw APIError.invalidArgument("refId_required");
18+
}
19+
20+
try {
21+
const buffer = await artifactsBucket.download(refId);
22+
const mimeType = inferMimeType(refId);
23+
const dataUrl = `data:${mimeType};base64,${buffer.toString("base64")}`;
24+
25+
return { refId, mimeType, dataUrl };
26+
} catch (error) {
27+
logger.error("Failed to download artifact content", { err: error });
28+
throw APIError.notFound("artifact_content_unavailable");
29+
}
30+
},
31+
);
32+
33+
/**
34+
* inferMimeType derives the MIME type from the artifact reference.
35+
* PURPOSE: Ensure returned data URLs render correctly in browsers.
36+
*/
37+
function inferMimeType(refId: string): "image/png" | "image/jpeg" {
38+
if (refId.endsWith(".jpg") || refId.endsWith(".jpeg")) {
39+
return "image/jpeg";
40+
}
41+
return "image/png";
42+
}
43+

frontend/src/lib/api.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export async function startRun(params: run.StartRunRequest): Promise<run.StartRu
1515
*/
1616
export async function streamRunEvents(
1717
runId: string,
18-
onEvent: (event: run.RunEventMessage) => void,
18+
onEvent: (event: run.RunEventMessage) => void | Promise<void>,
1919
) {
2020
const client = await getEncoreClient();
2121
const stream = await client.run.stream(runId, { lastEventSeq: 0 });
@@ -27,7 +27,7 @@ export async function streamRunEvents(
2727
if (!active) {
2828
return;
2929
}
30-
onEvent(event);
30+
await onEvent(event);
3131
}
3232
} catch (error) {
3333
console.error("Run stream error:", error);
@@ -53,7 +53,7 @@ export async function streamRunEvents(
5353
*/
5454
export async function streamGraphEvents(
5555
runId: string,
56-
onEvent: (event: graph.GraphStreamEvent) => void,
56+
onEvent: (event: graph.GraphStreamEvent) => void | Promise<void>,
5757
) {
5858
const client = await getEncoreClient();
5959

@@ -100,7 +100,7 @@ export async function streamGraphEvents(
100100
return;
101101
}
102102
console.log("[Graph Stream] Received event from stream:", event);
103-
onEvent(event);
103+
await onEvent(event);
104104
}
105105
console.log("[Graph Stream] Stream ended (no more events)");
106106
} catch (error) {
@@ -134,3 +134,19 @@ export async function cancelRun(runId: string): Promise<run.CancelRunResponse> {
134134
const client = await getEncoreClient();
135135
return client.run.cancel(runId);
136136
}
137+
138+
/**
139+
* fetchArtifactDataUrl downloads artifact content and returns a browser-friendly data URL.
140+
* PURPOSE: Provide frontend rendering support for screenshots stored in object storage.
141+
*/
142+
export async function fetchArtifactDataUrl(refId: string): Promise<string | null> {
143+
const client = await getEncoreClient();
144+
145+
try {
146+
const response = await client.artifacts.getArtifactContent({ refId });
147+
return response.dataUrl ?? null;
148+
} catch (error) {
149+
console.error("Failed to fetch artifact data URL", { error, refId });
150+
return null;
151+
}
152+
}

frontend/src/lib/encore-client.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Code generated by the Encore v1.50.6 client generator. DO NOT EDIT.
1+
// Code generated by the Encore v1.51.6 client generator. DO NOT EDIT.
22

33
// Disable eslint, jshint, and jslint for this file.
44
/* eslint-disable */
@@ -33,6 +33,7 @@ const BROWSER = typeof globalThis === "object" && ("window" in globalThis);
3333
*/
3434
export default class Client {
3535
public readonly appinfo: appinfo.ServiceClient
36+
public readonly artifacts: artifacts.ServiceClient
3637
public readonly graph: graph.ServiceClient
3738
public readonly run: run.ServiceClient
3839
public readonly steering: steering.ServiceClient
@@ -51,6 +52,7 @@ export default class Client {
5152
this.options = options ?? {}
5253
const base = new BaseClient(this.target, this.options)
5354
this.appinfo = new appinfo.ServiceClient(base)
55+
this.artifacts = new artifacts.ServiceClient(base)
5456
this.graph = new graph.ServiceClient(base)
5557
this.run = new run.ServiceClient(base)
5658
this.steering = new steering.ServiceClient(base)
@@ -182,6 +184,44 @@ export namespace appinfo {
182184
}
183185
}
184186

187+
export namespace artifacts {
188+
export interface GetArtifactContentResponse {
189+
refId: string
190+
mimeType: string
191+
dataUrl: string
192+
}
193+
194+
export class ServiceClient {
195+
private baseClient: BaseClient
196+
197+
constructor(baseClient: BaseClient) {
198+
this.baseClient = baseClient
199+
this.getArtifactContent = this.getArtifactContent.bind(this)
200+
}
201+
202+
/**
203+
* getArtifactContent returns inline screenshot data for a given artifact refId.
204+
* PURPOSE: Enable frontend clients to render screenshots without direct bucket access.
205+
*/
206+
public async getArtifactContent(params: {
207+
/**
208+
* getArtifactContent returns inline screenshot data for a given artifact refId.
209+
* PURPOSE: Enable frontend clients to render screenshots without direct bucket access.
210+
*/
211+
refId?: string
212+
}): Promise<GetArtifactContentResponse> {
213+
// Convert our params into the objects we need for the request
214+
const query = makeRecord<string, string | string[]>({
215+
refId: params.refId,
216+
})
217+
218+
// Now make the actual call to the API
219+
const resp = await this.baseClient.callTypedAPI("GET", `/artifacts/content`, undefined, {query})
220+
return await resp.json() as GetArtifactContentResponse
221+
}
222+
}
223+
}
224+
185225
export namespace graph {
186226
export interface GetScreensResponse {
187227
screens: {
@@ -653,7 +693,7 @@ class BaseClient {
653693
// Add User-Agent header if the script is running in the server
654694
// because browsers do not allow setting User-Agent headers to requests
655695
if (!BROWSER) {
656-
this.headers["User-Agent"] = "screengraph-ovzi-Generated-TS-Client (Encore/v1.50.6)";
696+
this.headers["User-Agent"] = "screengraph-ovzi-Generated-TS-Client (Encore/v1.51.6)";
657697
}
658698

659699
this.requestInit = options.requestInit ?? {};

0 commit comments

Comments
 (0)