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: 1 addition & 1 deletion tasks/async-word-counter/PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The workspace has already been scaffolded. Begin by reading README.md, then do a
1. A user types text into an input and clicks submit.
2. Submitting enqueues a background job (do the counting in the job — not inline in the request handler) and immediately adds a row for it whose `data-status` is `"processing"`.
3. The background job counts the words and stores the result in a key/value block keyed by the job id. **Count by whitespace runs only:** trim the text, then split on any run of whitespace (spaces, tabs) — so `one two three four five` is 5, and `" a b "` (leading/trailing + double spaces) is 2. A naive `split(' ')` that counts empty gaps is wrong.
- **Punctuation is part of a word, not a separator:** only whitespace separates words, so `hello,world foo.bar-baz!` is **3** words, not 5.
- **Punctuation is part of a word, not a separator:** only whitespace separates words, so `hello,world foo.bar baz` is **3** words, not 5.
- **Unicode and emoji tokens each count as one word:** `café 日本語 🙂 naïve` is **4** words. Count each maximal run of non-whitespace as one word — do **not** use `\w+` / `\W+`, which miscount punctuation and non-ASCII/emoji characters.
4. **Polling:** the frontend polls for the result; when it's ready the row's `data-status` becomes `"done"` and the row shows the word count.
5. **Persistence (including in-flight jobs):** persist each submission to the key/value block **when you enqueue it** (status `"processing"`, keyed by job id) — not only when it finishes. On load, **restore the whole list from the store** (every job, including still-`processing` ones) and resume polling — not just the most recent submission. A row reloaded **while still processing** must reappear and still resolve to `"done"` with its count. (An app that tracks the job list only in client memory loses an in-flight row on reload — that fails.)
Expand Down
18 changes: 15 additions & 3 deletions tasks/async-word-counter/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,18 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors.
// Per-test no-error gate: uncaught page errors and genuine console errors.
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand All @@ -37,8 +46,11 @@ test.describe('async-word-counter', () => {
await expect(page.getByTestId('wc-input')).toBeVisible();
await expect(page.getByTestId('wc-submit')).toBeVisible();
// Enforce the wc-list container contract (in the PROMPT selector table)
// so an impl can't omit the list wrapper and still pass.
await expect(page.getByTestId('wc-list')).toBeVisible();
// so an impl can't omit the list wrapper and still pass. Use toBeAttached
// (not toBeVisible): an empty list container legitimately has zero layout
// height, so toBeVisible() would fail a correct app, while toBeAttached
// still enforces that the wrapper exists.
await expect(page.getByTestId('wc-list')).toBeAttached();

expect(errors, `page errors: ${errors.join(' | ')}`).toEqual([]);
});
Expand Down
11 changes: 10 additions & 1 deletion tasks/auth-notes/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: collect ONLY uncaught page errors. Console warnings
// Per-test no-error gate: collect uncaught page errors and genuine console errors. Console warnings
// and 4xx/5xx responses are intentionally NOT failures — the local dev server
// returns HTTP 200 for JSON-RPC errors and 4xx pre-auth, which are expected.
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down
11 changes: 10 additions & 1 deletion tasks/cognito-profile/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors. The local dev server
// Per-test no-error gate: uncaught page errors and genuine console errors. The local dev server
// returns HTTP 200 for JSON-RPC errors, so legitimate pre-auth errors never
// trip the gate.
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down
11 changes: 10 additions & 1 deletion tasks/collab-presence-board/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors.
// Per-test no-error gate: uncaught page errors and genuine console errors.
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down
2 changes: 1 addition & 1 deletion tasks/email-digest/PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Build a scheduled email-digest feature in this AWS Blocks app. A recurring job i

## Setup (do this first)

The workspace has already been scaffolded. Begin by reading README.md, then do all your edits in this workspace.
The workspace has already been scaffolded. Begin by reading README.md, then do all your edits in this workspace. Remove any starter/scaffold demo code and UI you don't need for this task, so the workspace stays scoped to the digest feature.

## Requirements

Expand Down
11 changes: 10 additions & 1 deletion tasks/email-digest/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors.
// Per-test no-error gate: uncaught page errors and genuine console errors.
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down
11 changes: 10 additions & 1 deletion tasks/file-gallery/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors (not console warnings, not
// Per-test no-error gate: uncaught page errors and genuine console errors (not console warnings, not
// 4xx/5xx responses).
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down
11 changes: 10 additions & 1 deletion tasks/kb-chat-agent/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors. JSON-RPC error envelopes
// Per-test no-error gate: uncaught page errors and genuine console errors. JSON-RPC error envelopes
// come back as HTTP 200, so they are intentionally not treated as failures.
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down
29 changes: 28 additions & 1 deletion tasks/observability-api/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,18 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors.
// Per-test no-error gate: uncaught page errors and genuine console errors.
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down Expand Up @@ -243,6 +252,24 @@ test.describe('observability-api', () => {
expect(b2.result ?? null).toBeNull();
});

test('api.echo round-trips falsy-but-present arguments (0, false, null, "") — no truthiness footgun', async ({ request }) => {
// A supplied argument that is FALSY is still present: only an absent/empty params list is
// "missing" (covered above). An impl that detects "missing" via truthiness (`if (!arg)`)
// wrongly rejects these — echo must key off arity (params.length), not the value's truthiness.
const falsy: Array<number | boolean | null | string> = [0, false, null, ''];
let id = 530;
for (const v of falsy) {
const res = await request.post(`${BASE}/aws-blocks/api`, {
headers: { 'Content-Type': 'application/json' },
data: { jsonrpc: '2.0', method: 'api.echo', params: [v], id: id++ },
});
expect(res.ok(), `HTTP ${res.status()} from api.echo(${JSON.stringify(v)})`).toBe(true);
const body = await res.json();
expect(body.error, `echo(${JSON.stringify(v)}) must not error: ${JSON.stringify(body.error)}`).toBeFalsy();
expect(body.result, `echo(${JSON.stringify(v)}) result`).toEqual({ echo: v });
}
});

test('a malformed (array/batch) request returns an InvalidRequest error envelope, not a 5xx', async ({ request }) => {
// The framework does not support batch; an array body is an Invalid
// Request. The contract is a clean JSON-RPC error envelope, never a crash.
Expand Down
32 changes: 31 additions & 1 deletion tasks/oidc-dsql-notes/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors (persists across the
// Per-test no-error gate: uncaught page errors and genuine console errors (persists across the
// sign-in redirect navigations).
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down Expand Up @@ -271,4 +280,25 @@ test.describe('oidc-dsql-notes', () => {

expect(errors, `page errors: ${errors.join(' | ')}`).toEqual([]);
});

test('unauthenticated JSON-RPC note access is refused (no session → no note data)', async ({ request }) => {
// No session cookie. Notes are OIDC-gated and per-user, so an unauthenticated list/add must
// NOT return note data. Method names aren't fixed by the contract (they're the agent's choice),
// so an unknown method also yields a JSON-RPC error envelope — this probe never spuriously
// fails; it only fails an impl that actually serves or mutates notes with no session.
for (const method of ['api.listNotes', 'api.addNote']) {
const res = await request.post(`${BASE}/aws-blocks/api`, {
headers: { 'Content-Type': 'application/json' },
data: { jsonrpc: '2.0', method, params: method === 'api.addNote' ? [`unauth-probe-${RUN}`] : [], id: 41 },
});
expect(res.status(), `unexpected HTTP ${res.status()} from ${method}`).toBeLessThan(500);
const body = await res.json().catch(() => null);
// A non-JSON response (e.g. an auth redirect to the sign-in page) is itself a refusal: it
// carried no note data. Only a JSON body is held to the error-envelope contract.
if (body && typeof body === 'object') {
expect(body.error, `${method} with no session must yield a JSON-RPC error envelope`).toBeTruthy();
expect(body.result ?? null, `${method} must not return note data unauthenticated`).toBeNull();
}
}
});
});
13 changes: 12 additions & 1 deletion tasks/sql-kb-catalog/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ const RUN = process.env.RUN_ID || String(Date.now());
let seq = 0;
const uniq = (base: string) => `${base}-${RUN}-${++seq}-${Date.now()}`;

// Per-test no-error gate: ONLY uncaught page errors.
// Per-test no-error gate: uncaught page errors and genuine console errors.
function watchErrors(page: Page, sink: string[] = []): string[] {
page.on('pageerror', (err) => sink.push(String(err)));
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Exclude benign browser-generated noise (not an app JS fault): failed resource loads / HTTP
// status errors (favicon, pre-auth 4xx, JSON-RPC-over-HTTP), WebSocket lifecycle, and dev-mode
// "Warning:" logs. A genuine console error still fails the gate, asserted empty end-of-test.
if (/Failed to load resource|net::ERR|favicon|WebSocket|^\s*Warning:/i.test(text)) return;
sink.push(`console.error: ${text}`);
});
return sink;
}

Expand Down Expand Up @@ -88,6 +97,7 @@ test.describe('sql-kb-catalog', () => {
await expect(page.getByTestId('kb-result')).toHaveCount(0);
await searchFaq(page, 'return refund policy');
await expect(page.getByTestId('kb-result').first()).toBeVisible({ timeout: T });
await expect(page.getByTestId('kb-result').first()).toContainText(/refund/i, { timeout: T });

expect(errors, `page errors: ${errors.join(' | ')}`).toEqual([]);
});
Expand All @@ -99,6 +109,7 @@ test.describe('sql-kb-catalog', () => {
await expect(page.getByTestId('kb-result')).toHaveCount(0);
await searchFaq(page, 'refund');
await expect(page.getByTestId('kb-result').first()).toBeVisible({ timeout: T });
await expect(page.getByTestId('kb-result').first()).toContainText(/refund/i, { timeout: T });

expect(errors, `page errors: ${errors.join(' | ')}`).toEqual([]);
});
Expand Down
Loading