webui: Add test for sketch-app-shell component

Created a basic test for the top-level Sketch App Shell component with API mocking. Reorganized mock data to be shared between tests and demo.

Co-Authored-By: sketch
diff --git a/webui/src/web-components/demo/mocks/fixtures/dummy.ts b/webui/src/web-components/demo/mocks/fixtures/dummy.ts
deleted file mode 100644
index e45371f..0000000
--- a/webui/src/web-components/demo/mocks/fixtures/dummy.ts
+++ /dev/null
@@ -1,372 +0,0 @@
-import { AgentMessage, State } from "../../../../types";
-
-export const initialMessages: AgentMessage[] = [
-  {
-    type: "user",
-    end_of_turn: false,
-    content:
-      "add a line to dummy.txt. Doesn't matter what it is and don't bother running tests it's just a dummy repo. Please commit afterward",
-    timestamp: "2025-04-24T10:32:00.679873+01:00",
-    conversation_id: "",
-    idx: 0,
-  },
-  {
-    type: "agent",
-    end_of_turn: false,
-    content:
-      "I'll help you add a line to dummy.txt and commit the change. Let me first check if this file exists and create it if needed.",
-    tool_calls: [
-      {
-        name: "title",
-        input: '{"title":"Add a line to dummy.txt and commit the change"}',
-        tool_call_id: "toolu_01HSQ8xb9Ba8oP1gJPv1eD2s",
-      },
-    ],
-    timestamp: "2025-04-24T10:32:03.988439+01:00",
-    conversation_id: "37s-g6xg",
-    usage: {
-      input_tokens: 4,
-      cache_creation_input_tokens: 2228,
-      cache_read_input_tokens: 0,
-      output_tokens: 92,
-      cost_usd: 0.009747,
-    },
-    start_time: "2025-04-24T10:32:00.680125+01:00",
-    end_time: "2025-04-24T10:32:03.988434+01:00",
-    elapsed: 3308355125,
-    idx: 1,
-  },
-  {
-    type: "tool",
-    end_of_turn: false,
-    content: "",
-    tool_name: "title",
-    input: '{"title":"Add a line to dummy.txt and commit the change"}',
-    tool_result: "Title set to: Add a line to dummy.txt and commit the change",
-    tool_call_id: "toolu_01HSQ8xb9Ba8oP1gJPv1eD2s",
-    timestamp: "2025-04-24T10:32:03.988567+01:00",
-    conversation_id: "37s-g6xg",
-    start_time: "2025-04-24T10:32:03.988553+01:00",
-    end_time: "2025-04-24T10:32:03.988566+01:00",
-    elapsed: 13500,
-    idx: 2,
-  },
-  {
-    type: "agent",
-    end_of_turn: false,
-    content: "",
-    tool_calls: [
-      {
-        name: "bash",
-        input: '{"command":"ls -la"}',
-        tool_call_id: "toolu_01RtkhetY8bqC6U1QBfVbukS",
-      },
-    ],
-    timestamp: "2025-04-24T10:32:06.146332+01:00",
-    conversation_id: "37s-g6xg",
-    usage: {
-      input_tokens: 6,
-      cache_creation_input_tokens: 116,
-      cache_read_input_tokens: 2228,
-      output_tokens: 54,
-      cost_usd: 0.0019314,
-    },
-    start_time: "2025-04-24T10:32:04.007226+01:00",
-    end_time: "2025-04-24T10:32:06.146325+01:00",
-    elapsed: 2139128375,
-    idx: 3,
-  },
-  {
-    type: "tool",
-    end_of_turn: false,
-    content: "",
-    tool_name: "bash",
-    input: '{"command":"ls -la"}',
-    tool_result:
-      "total 1712\ndrwxr-xr-x   29 pokey  staff     928 Apr 15 13:06 .\ndrwxr-xr-x  397 pokey  staff   12704 Apr 23 09:53 ..\n-rw-r--r--@   1 pokey  staff     292 Jan  8  2024 .editorconfig\ndrwxr-xr-x   17 pokey  staff     544 Apr 24 10:32 .git\ndrwxr-xr-x@   3 pokey  staff      96 Jan  8  2024 .github\n-rw-r--r--@   1 pokey  staff     777 Jan  8  2024 .gitignore\n-rw-r--r--@   1 pokey  staff       0 Apr 15 13:06 .PROMPT_DRAFT.md\n-rw-------    1 pokey  staff   11811 Oct  4  2022 .tags\n-rw-r--r--@   1 pokey  staff     762 Jan  8  2024 .travis.yml\ndrwxr-xr-x    3 pokey  staff      96 Apr  9 10:51 .vscode\n-rw-r--r--@   1 pokey  staff     156 Jan  8  2024 AUTHORS.rst\n-rw-r--r--@   1 pokey  staff    3249 Jan  8  2024 CONTRIBUTING.rst\ndrwxr-xr-x@  12 pokey  staff     384 Jan  8  2024 docs\n-rw-r--r--    1 pokey  staff    6644 Apr 23 15:31 dummy.txt\n-rw-r--r--@   1 pokey  staff      89 Jan  8  2024 HISTORY.rst\n-rw-r--r--@   1 pokey  staff    1070 Jan  8  2024 LICENSE\n-rw-r--r--@   1 pokey  staff    2303 Jan  8  2024 Makefile\n-rw-r--r--@   1 pokey  staff     264 Jan  8  2024 MANIFEST.in\n-rw-r--r--    1 pokey  staff  794413 Mar  7  2017 myVim.log\n-rw-r--r--@   1 pokey  staff    1043 Apr  8 12:21 README.rst\n-rw-r--r--@   1 pokey  staff     145 Jan  8  2024 requirements_dev.txt\n-rw-r--r--@   1 pokey  staff     342 Jan  8  2024 setup.cfg\n-rw-r--r--@   1 pokey  staff    1559 Jan  8  2024 setup.py\ndrwxr-xr-x    6 pokey  staff     192 Jan  8  2024 spaghetti\ndrwxr-xr-x    9 pokey  staff     288 Feb 26  2017 spaghetti.egg-info\ndrwxr-xr-x@   5 pokey  staff     160 Apr  4 19:07 tests\n-rw-r--r--@   1 pokey  staff     430 Jan  8  2024 tox.ini\n-rw-r--r--@   1 pokey  staff    3753 Jan  8  2024 travis_pypi_setup.py\ndrwxr-xr-x    2 pokey  staff      64 Mar 11  2017 UltiSnips\n",
-    tool_call_id: "toolu_01RtkhetY8bqC6U1QBfVbukS",
-    timestamp: "2025-04-24T10:32:06.176176+01:00",
-    conversation_id: "37s-g6xg",
-    start_time: "2025-04-24T10:32:06.146414+01:00",
-    end_time: "2025-04-24T10:32:06.176175+01:00",
-    elapsed: 29760750,
-    idx: 4,
-  },
-  {
-    type: "agent",
-    end_of_turn: false,
-    content:
-      "I see that dummy.txt already exists. Let me add a new line to it.",
-    tool_calls: [
-      {
-        name: "patch",
-        input:
-          '{"path":"/Users/pokey/src/spaghetti/dummy.txt","patches":[{"operation":"append_eof","newText":"\\nAdding a new dummy line here - requested change"}]}',
-        tool_call_id: "toolu_01KjY2a2MWhfvwy6Pmci8SX2",
-      },
-    ],
-    timestamp: "2025-04-24T10:32:09.724348+01:00",
-    conversation_id: "37s-g6xg",
-    usage: {
-      input_tokens: 6,
-      cache_creation_input_tokens: 970,
-      cache_read_input_tokens: 2344,
-      output_tokens: 141,
-      cost_usd: 0.0064737,
-    },
-    start_time: "2025-04-24T10:32:06.195746+01:00",
-    end_time: "2025-04-24T10:32:09.724335+01:00",
-    elapsed: 3528638042,
-    idx: 5,
-  },
-  {
-    type: "tool",
-    end_of_turn: false,
-    content: "",
-    tool_name: "patch",
-    input:
-      '{"path":"/Users/pokey/src/spaghetti/dummy.txt","patches":[{"operation":"append_eof","newText":"\\nAdding a new dummy line here - requested change"}]}',
-    tool_result: "- Applied all patches\n",
-    tool_call_id: "toolu_01KjY2a2MWhfvwy6Pmci8SX2",
-    timestamp: "2025-04-24T10:32:09.728267+01:00",
-    conversation_id: "37s-g6xg",
-    start_time: "2025-04-24T10:32:09.725039+01:00",
-    end_time: "2025-04-24T10:32:09.728265+01:00",
-    elapsed: 3225875,
-    idx: 6,
-  },
-  {
-    type: "agent",
-    end_of_turn: false,
-    content: "Now let me commit this change:",
-    tool_calls: [
-      {
-        name: "bash",
-        input:
-          '{"command":"GIT_AUTHOR_NAME=\\"Pokey Rule\\" GIT_AUTHOR_EMAIL=\\"755842+pokey@users.noreply.github.com\\" git add dummy.txt && git commit -m \\"Add a new line to dummy.txt\\" -m \\"Co-Authored-By: sketch\\" -m \\"Add a line to dummy.txt. Doesn\'t matter what it is and don\'t bother running tests it\'s just a dummy repo.\\""}',
-        tool_call_id: "toolu_01NQXuuU4JdUHn29S35oGLxF",
-      },
-    ],
-    timestamp: "2025-04-24T10:32:13.306331+01:00",
-    conversation_id: "37s-g6xg",
-    usage: {
-      input_tokens: 6,
-      cache_creation_input_tokens: 146,
-      cache_read_input_tokens: 3314,
-      output_tokens: 160,
-      cost_usd: 0.0039597,
-    },
-    start_time: "2025-04-24T10:32:09.76053+01:00",
-    end_time: "2025-04-24T10:32:13.306203+01:00",
-    elapsed: 3545723083,
-    idx: 7,
-  },
-  {
-    type: "tool",
-    end_of_turn: false,
-    content: "",
-    tool_name: "bash",
-    input:
-      '{"command":"GIT_AUTHOR_NAME=\\"Pokey Rule\\" GIT_AUTHOR_EMAIL=\\"755842+pokey@users.noreply.github.com\\" git add dummy.txt && git commit -m \\"Add a new line to dummy.txt\\" -m \\"Co-Authored-By: sketch\\" -m \\"Add a line to dummy.txt. Doesn\'t matter what it is and don\'t bother running tests it\'s just a dummy repo.\\""}',
-    tool_result:
-      "branchless: processing 2 updates: branch main, ref HEAD\nbranchless: processed commit: 898178a Add a new line to dummy.txt\n[main 898178a] Add a new line to dummy.txt\n 1 file changed, 2 insertions(+), 1 deletion(-)\n",
-    tool_call_id: "toolu_01NQXuuU4JdUHn29S35oGLxF",
-    timestamp: "2025-04-24T10:32:13.652683+01:00",
-    conversation_id: "37s-g6xg",
-    start_time: "2025-04-24T10:32:13.307345+01:00",
-    end_time: "2025-04-24T10:32:13.652678+01:00",
-    elapsed: 345337875,
-    idx: 8,
-  },
-  {
-    type: "commit",
-    end_of_turn: false,
-    content: "",
-    commits: [
-      {
-        hash: "898178a0c6681ce2b641cbe8994c22ed8bbb7dd7",
-        subject: "Add a new line to dummy.txt",
-        body: "Co-Authored-By: sketch\n\nAdd a line to dummy.txt. Doesn't matter what it is and don't bother running tests it's just a dummy repo.",
-      },
-    ],
-    timestamp: "2025-04-24T10:32:13.690825+01:00",
-    conversation_id: "",
-    idx: 9,
-  },
-  {
-    type: "agent",
-    end_of_turn: false,
-    content: "",
-    tool_calls: [
-      {
-        name: "done",
-        input:
-          '{"checklist_items":{"git_commit":{"status":"yes","description":"Created a git commit for the code changes with the appropriate commit message that includes \'Co-Authored-By: sketch\' and the original user prompt."},"wrote_tests":{"status":"not applicable","description":"No functional code was changed, just added a line to a dummy file."},"passes_tests":{"status":"not applicable","description":"User explicitly mentioned not to run tests as this is just a dummy repo."},"code_reviewed":{"status":"not applicable","description":"The change was trivial - just adding a line to a dummy.txt file."}}}',
-        tool_call_id: "toolu_01YMBDmBoCcE5tfZvbXAgWEa",
-      },
-    ],
-    timestamp: "2025-04-24T10:32:19.07007+01:00",
-    conversation_id: "37s-g6xg",
-    usage: {
-      input_tokens: 6,
-      cache_creation_input_tokens: 244,
-      cache_read_input_tokens: 3460,
-      output_tokens: 241,
-      cost_usd: 0.005586,
-    },
-    start_time: "2025-04-24T10:32:13.74753+01:00",
-    end_time: "2025-04-24T10:32:19.070058+01:00",
-    elapsed: 5322601792,
-    idx: 10,
-  },
-  {
-    type: "tool",
-    end_of_turn: false,
-    content: "",
-    tool_name: "done",
-    input:
-      '{"checklist_items":{"git_commit":{"status":"yes","description":"Created a git commit for the code changes with the appropriate commit message that includes \'Co-Authored-By: sketch\' and the original user prompt."},"wrote_tests":{"status":"not applicable","description":"No functional code was changed, just added a line to a dummy file."},"passes_tests":{"status":"not applicable","description":"User explicitly mentioned not to run tests as this is just a dummy repo."},"code_reviewed":{"status":"not applicable","description":"The change was trivial - just adding a line to a dummy.txt file."}}}',
-    tool_result:
-      "codereview tool has not been run for commit 898178a0c6681ce2b641cbe8994c22ed8bbb7dd7",
-    tool_error: true,
-    tool_call_id: "toolu_01YMBDmBoCcE5tfZvbXAgWEa",
-    timestamp: "2025-04-24T10:32:19.121365+01:00",
-    conversation_id: "37s-g6xg",
-    start_time: "2025-04-24T10:32:19.070249+01:00",
-    end_time: "2025-04-24T10:32:19.121358+01:00",
-    elapsed: 51109583,
-    idx: 11,
-  },
-  {
-    type: "agent",
-    end_of_turn: false,
-    content: "Let me run a code review as required:",
-    tool_calls: [
-      {
-        name: "codereview",
-        input: "{}",
-        tool_call_id: "toolu_01Fgg5vaURskyxGcbwHoBu7z",
-      },
-    ],
-    timestamp: "2025-04-24T10:32:21.918149+01:00",
-    conversation_id: "37s-g6xg",
-    usage: {
-      input_tokens: 7,
-      cache_creation_input_tokens: 253,
-      cache_read_input_tokens: 3704,
-      output_tokens: 54,
-      cost_usd: 0.0028909499999999998,
-    },
-    start_time: "2025-04-24T10:32:19.14049+01:00",
-    end_time: "2025-04-24T10:32:21.918133+01:00",
-    elapsed: 2777681917,
-    idx: 12,
-  },
-  {
-    type: "tool",
-    end_of_turn: false,
-    content: "",
-    tool_name: "codereview",
-    input: "{}",
-    tool_result: "OK",
-    tool_call_id: "toolu_01Fgg5vaURskyxGcbwHoBu7z",
-    timestamp: "2025-04-24T10:32:22.516658+01:00",
-    conversation_id: "37s-g6xg",
-    start_time: "2025-04-24T10:32:21.918261+01:00",
-    end_time: "2025-04-24T10:32:22.516654+01:00",
-    elapsed: 598402125,
-    idx: 13,
-  },
-  {
-    type: "agent",
-    end_of_turn: false,
-    content: "Now let me try the done call again:",
-    tool_calls: [
-      {
-        name: "done",
-        input:
-          '{"checklist_items":{"git_commit":{"status":"yes","description":"Created a git commit for the code changes with the appropriate commit message that includes \'Co-Authored-By: sketch\' and the original user prompt."},"wrote_tests":{"status":"not applicable","description":"No functional code was changed, just added a line to a dummy file."},"passes_tests":{"status":"not applicable","description":"User explicitly mentioned not to run tests as this is just a dummy repo."},"code_reviewed":{"status":"yes","description":"Code review completed and no issues found. The change was trivial - just adding a line to a dummy.txt file."}}}',
-        tool_call_id: "toolu_016TBxz5Ma8y3if3rY4DZBXE",
-      },
-    ],
-    timestamp: "2025-04-24T10:32:26.926407+01:00",
-    conversation_id: "37s-g6xg",
-    usage: {
-      input_tokens: 6,
-      cache_creation_input_tokens: 60,
-      cache_read_input_tokens: 3957,
-      output_tokens: 217,
-      cost_usd: 0.0046851,
-    },
-    start_time: "2025-04-24T10:32:22.534359+01:00",
-    end_time: "2025-04-24T10:32:26.926391+01:00",
-    elapsed: 4392093250,
-    idx: 14,
-  },
-  {
-    type: "tool",
-    end_of_turn: false,
-    content: "",
-    tool_name: "done",
-    input:
-      '{"checklist_items":{"git_commit":{"status":"yes","description":"Created a git commit for the code changes with the appropriate commit message that includes \'Co-Authored-By: sketch\' and the original user prompt."},"wrote_tests":{"status":"not applicable","description":"No functional code was changed, just added a line to a dummy file."},"passes_tests":{"status":"not applicable","description":"User explicitly mentioned not to run tests as this is just a dummy repo."},"code_reviewed":{"status":"yes","description":"Code review completed and no issues found. The change was trivial - just adding a line to a dummy.txt file."}}}',
-    tool_result:
-      "Please ask the user to review your work. Be concise - users are more likely to read shorter comments.",
-    tool_call_id: "toolu_016TBxz5Ma8y3if3rY4DZBXE",
-    timestamp: "2025-04-24T10:32:26.978771+01:00",
-    conversation_id: "37s-g6xg",
-    start_time: "2025-04-24T10:32:26.92661+01:00",
-    end_time: "2025-04-24T10:32:26.978758+01:00",
-    elapsed: 52149125,
-    idx: 15,
-  },
-  {
-    type: "agent",
-    end_of_turn: true,
-    content:
-      'I\'ve completed your request:\n\n1. Added a new line to dummy.txt: "Adding a new dummy line here - requested change"\n2. Created a git commit with the change using the specified author information\n3. Included "Co-Authored-By: sketch" in the commit message as required\n\nThe change has been successfully committed to the repository. Is there anything else you\'d like me to help with?',
-    timestamp: "2025-04-24T10:32:29.072661+01:00",
-    conversation_id: "37s-g6xg",
-    usage: {
-      input_tokens: 5,
-      cache_creation_input_tokens: 250,
-      cache_read_input_tokens: 4017,
-      output_tokens: 92,
-      cost_usd: 0.0035376,
-    },
-    start_time: "2025-04-24T10:32:26.99749+01:00",
-    end_time: "2025-04-24T10:32:29.072654+01:00",
-    elapsed: 2075193375,
-    turnDuration: 28393844125,
-    idx: 16,
-  },
-];
-
-export const initialState: State = {
-  message_count: initialMessages.length,
-  total_usage: {
-    start_time: "2025-04-24T10:32:00.679057+01:00",
-    messages: 8,
-    input_tokens: 46,
-    output_tokens: 1051,
-    cache_read_input_tokens: 23024,
-    cache_creation_input_tokens: 4267,
-    total_cost_usd: 0.038811450000000004,
-    tool_uses: {
-      bash: 2,
-      codereview: 1,
-      done: 2,
-      patch: 1,
-      title: 1,
-    },
-  },
-  initial_commit: "a6c5a08a451ef1082774a7affb6af58775e7bc16",
-  title: "Add a line to dummy.txt and commit the change",
-  hostname: "MacBook-Pro-9.local",
-  working_dir: "/Users/pokey/src/spaghetti",
-  os: "darwin",
-  git_origin: "git@github.com:pokey/spaghetti.git",
-  inside_hostname: "MacBook-Pro-9.local",
-  inside_os: "darwin",
-  inside_working_dir: "/Users/pokey/src/spaghetti",
-};
diff --git a/webui/src/web-components/demo/mocks/handlers.ts b/webui/src/web-components/demo/mocks/handlers.ts
index 21bacd7..c05a6c4 100644
--- a/webui/src/web-components/demo/mocks/handlers.ts
+++ b/webui/src/web-components/demo/mocks/handlers.ts
@@ -1,5 +1,5 @@
 import { http, HttpResponse, delay } from "msw";
-import { initialState, initialMessages } from "./fixtures/dummy";
+import { initialState, initialMessages } from "../../../fixtures/dummy";
 
 // Mock state updates for long-polling simulation
 let currentState = { ...initialState };
diff --git a/webui/src/web-components/sketch-app-shell.test.ts b/webui/src/web-components/sketch-app-shell.test.ts
new file mode 100644
index 0000000..0b3ae6a
--- /dev/null
+++ b/webui/src/web-components/sketch-app-shell.test.ts
@@ -0,0 +1,88 @@
+import { test, expect } from "@sand4rt/experimental-ct-web";
+import { SketchAppShell } from "./sketch-app-shell";
+import { initialMessages, initialState } from "../fixtures/dummy";
+
+test("renders app shell with mocked API", async ({ page, mount }) => {
+  // Mock the state API response
+  await page.route("**/state", async (route) => {
+    await route.fulfill({ json: initialState });
+  });
+
+  // Mock the messages API response
+  await page.route("**/messages*", async (route) => {
+    const url = new URL(route.request().url());
+    const startIndex = parseInt(url.searchParams.get("start") || "0");
+    await route.fulfill({ json: initialMessages.slice(startIndex) });
+  });
+
+  // Mount the component
+  const component = await mount(SketchAppShell);
+
+  // Wait for initial data to load
+  await page.waitForTimeout(500);
+
+  // Verify the title is displayed correctly
+  await expect(component.locator(".chat-title")).toContainText(
+    initialState.title,
+  );
+
+  // Verify core components are rendered
+  await expect(component.locator("sketch-container-status")).toBeVisible();
+  await expect(component.locator("sketch-timeline")).toBeVisible();
+  await expect(component.locator("sketch-chat-input")).toBeVisible();
+  await expect(component.locator("sketch-view-mode-select")).toBeVisible();
+
+  // Default view should be chat view
+  await expect(component.locator(".chat-view.view-active")).toBeVisible();
+});
+
+const emptyState = {
+  message_count: 0,
+  total_usage: {
+    start_time: "2025-04-25T19:07:24.94241+01:00",
+    messages: 0,
+    input_tokens: 0,
+    output_tokens: 0,
+    cache_read_input_tokens: 0,
+    cache_creation_input_tokens: 0,
+    total_cost_usd: 0,
+    tool_uses: {},
+  },
+  initial_commit: "08e2cf2eaf043df77f8468d90bb21d0083de2132",
+  title: "",
+  hostname: "MacBook-Pro-9.local",
+  working_dir: "/Users/pokey/src/sketch",
+  os: "darwin",
+  git_origin: "git@github.com:boldsoftware/sketch.git",
+  inside_hostname: "MacBook-Pro-9.local",
+  inside_os: "darwin",
+  inside_working_dir: "/Users/pokey/src/sketch",
+};
+
+test("renders app shell with empty state", async ({ page, mount }) => {
+  // Mock the state API response
+  await page.route("**/state", async (route) => {
+    await route.fulfill({ json: emptyState });
+  });
+
+  // Mock the messages API response
+  await page.route("**/messages*", async (route) => {
+    await route.fulfill({ json: [] });
+  });
+
+  // Mount the component
+  const component = await mount(SketchAppShell);
+
+  // Wait for initial data to load
+  await page.waitForTimeout(500);
+
+  // Verify the title is displayed correctly
+  await expect(component.locator(".chat-title")).toContainText(
+    emptyState.title,
+  );
+
+  // Verify core components are rendered
+  await expect(component.locator("sketch-container-status")).toBeVisible();
+  await expect(component.locator("sketch-chat-input")).toBeVisible();
+  await expect(component.locator("sketch-view-mode-select")).toBeVisible();
+});