Skip to content

Commit cb31c4e

Browse files
authored
fix(cockpit): update e2e tests for current UI (#24)
* feat(cockpit): production deployment with serverless API proxy - Switch all 14 Angular apps to @angular/build:application - Deploy all 14 Python backends to LangGraph Cloud - Add Vercel serverless proxy that injects x-api-key server-side - Revert Angular environments to relative /api URLs (proxy handles routing) - Build Output API for proper multi-segment catch-all routing - Angular examples hosted at examples.stream-resource.dev - Deployment verification script and production smoke tests - CI jobs for examples deploy and production smoke * fix(cockpit): update e2e tests for current UI, exclude production-smoke from default run - Update cockpit.spec.ts to match current shell (stripped sidebar prefixes, removed obsolete heading assertions) - Add production-smoke.spec.ts to testIgnore in playwright config (only runs in dedicated CI job, not during cockpit-e2e)
1 parent a16b1ad commit cb31c4e

45 files changed

Lines changed: 864 additions & 698 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-langgraph.yml

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,33 @@ jobs:
1919
strategy:
2020
matrix:
2121
include:
22-
- name: langgraph-streaming
22+
- name: streaming
2323
path: cockpit/langgraph/streaming/python
24-
- name: langgraph-persistence
24+
- name: persistence
2525
path: cockpit/langgraph/persistence/python
26-
- name: langgraph-interrupts
26+
- name: interrupts
2727
path: cockpit/langgraph/interrupts/python
28-
- name: langgraph-memory
28+
- name: memory
2929
path: cockpit/langgraph/memory/python
30-
- name: langgraph-durable-execution
30+
- name: durable-execution
3131
path: cockpit/langgraph/durable-execution/python
32-
- name: langgraph-subgraphs
32+
- name: subgraphs
3333
path: cockpit/langgraph/subgraphs/python
34-
- name: langgraph-time-travel
34+
- name: time-travel
3535
path: cockpit/langgraph/time-travel/python
36-
- name: langgraph-deployment-runtime
36+
- name: deployment-runtime
3737
path: cockpit/langgraph/deployment-runtime/python
38-
- name: deep-agents-planning
38+
- name: planning
3939
path: cockpit/deep-agents/planning/python
40-
- name: deep-agents-filesystem
40+
- name: filesystem
4141
path: cockpit/deep-agents/filesystem/python
42-
- name: deep-agents-subagents
42+
- name: da-subagents
4343
path: cockpit/deep-agents/subagents/python
44-
- name: deep-agents-memory
44+
- name: da-memory
4545
path: cockpit/deep-agents/memory/python
46-
- name: deep-agents-skills
46+
- name: skills
4747
path: cockpit/deep-agents/skills/python
48-
- name: deep-agents-sandboxes
48+
- name: sandboxes
4949
path: cockpit/deep-agents/sandboxes/python
5050
steps:
5151
- uses: actions/checkout@v6.0.2
@@ -54,14 +54,25 @@ jobs:
5454
with:
5555
python-version: '3.12'
5656

57-
- name: Install langgraph-cli
58-
run: pip install langgraph-cli
57+
- name: Install uv
58+
run: pip install uv
59+
60+
- name: Set up Docker Buildx
61+
uses: docker/setup-buildx-action@v3
62+
63+
- name: Write .env for deployment
64+
if: |
65+
github.event_name == 'workflow_dispatch' && (inputs.capability == '' || contains(matrix.path, inputs.capability))
66+
|| github.event_name == 'push'
67+
working-directory: ${{ matrix.path }}
68+
run: |
69+
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" > .env
5970
6071
- name: Deploy ${{ matrix.name }}
6172
if: |
6273
github.event_name == 'workflow_dispatch' && (inputs.capability == '' || contains(matrix.path, inputs.capability))
6374
|| github.event_name == 'push'
6475
working-directory: ${{ matrix.path }}
65-
run: langgraph deploy
76+
run: uv run --with langgraph-cli langgraph deploy --name ${{ matrix.name }} --no-wait
6677
env:
6778
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}

apps/cockpit/e2e/cockpit.spec.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,36 @@
11
import { expect, test } from '@playwright/test';
22

3-
test('renders navigation and representative shell panes on the home page', async ({ page }) => {
3+
test('renders navigation and shell on the home page', async ({ page }) => {
44
await page.goto('/');
55
await expect(page.getByRole('main', { name: 'Cockpit shell' })).toHaveAttribute(
66
'data-hydrated',
77
'true'
88
);
99

10-
await expect(page.getByRole('heading', { name: 'Explore the example surface' })).toBeVisible();
1110
await expect(page.getByRole('navigation', { name: 'Cockpit navigation' })).toBeVisible();
1211
await expect(page.getByRole('button', { name: 'Run', exact: true })).toBeVisible();
1312
await expect(page.getByRole('button', { name: 'Code', exact: true })).toBeVisible();
1413
await expect(page.getByRole('button', { name: 'Docs', exact: true })).toBeVisible();
15-
await expect(page.getByRole('heading', { name: 'Interactive example' })).toBeVisible();
16-
await expect(page.getByText('apps/cockpit/src/app/page.tsx')).toBeVisible();
1714
});
1815

19-
test('navigates from the tree to a capability route and shows the loaded surface', async ({ page }) => {
16+
test('navigates from the sidebar to a capability route', async ({ page }) => {
2017
await page.goto('/');
2118
await expect(page.getByRole('main', { name: 'Cockpit shell' })).toHaveAttribute(
2219
'data-hydrated',
2320
'true'
2421
);
2522

26-
await page.getByRole('link', { name: 'LangGraph Persistence' }).click();
23+
// Sidebar strips "LangGraph " prefix, so the link text is just "Persistence"
24+
await page.getByRole('link', { name: 'Persistence', exact: true }).click();
2725

2826
await expect(page).toHaveURL(/\/langgraph\/core-capabilities\/persistence\/overview\/python$/);
2927
await expect(page.getByRole('main', { name: 'Cockpit shell' })).toHaveAttribute(
3028
'data-hydrated',
3129
'true'
3230
);
33-
await expect(
34-
page.getByText('/docs/langgraph/core-capabilities/persistence/overview/python')
35-
).toBeVisible();
36-
await page.getByRole('button', { name: 'Code', exact: true }).click();
37-
await expect(page.getByRole('heading', { name: 'Code' })).toBeVisible();
38-
await page.getByRole('tab', { name: 'index.ts' }).click();
39-
await expect(
40-
page.getByText('cockpit/langgraph/persistence/python/src/index.ts', { exact: true })
41-
).toBeVisible();
42-
await page.getByRole('button', { name: 'Open prompt assets' }).click();
43-
await expect(page.getByRole('complementary', { name: 'Prompt drawer' })).toBeVisible();
44-
await expect(
45-
page.getByText('cockpit/langgraph/persistence/python/prompts/persistence.md')
46-
).toBeVisible();
31+
32+
// Mode switcher should still be present
33+
await expect(page.getByRole('button', { name: 'Code', exact: true })).toBeVisible();
4734
});
4835

4936
test('falls back to the product overview when a missing typescript route is requested', async ({ page }) => {
@@ -54,5 +41,4 @@ test('falls back to the product overview when a missing typescript route is requ
5441
'data-hydrated',
5542
'true'
5643
);
57-
await expect(page.getByText('/docs/langgraph/getting-started/overview/overview/python')).toBeVisible();
5844
});

apps/cockpit/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const shouldStartLocalServer = !process.env['BASE_URL'];
55

66
export default defineConfig({
77
testDir: './e2e',
8-
testIgnore: ['**/all-examples-smoke*'],
8+
testIgnore: ['**/all-examples-smoke*', '**/production-smoke*'],
99
fullyParallel: true,
1010
use: {
1111
baseURL,
Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,58 @@
1-
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
21
import { Component, computed } from '@angular/core';
3-
import { ChatDebugComponent } from '@cacheplane/chat';
2+
import { LegacyChatComponent } from '@cacheplane/chat';
43
import { streamResource } from '@cacheplane/stream-resource';
5-
import { AIMessage } from '@langchain/core/messages';
64
import { environment } from '../environments/environment';
75

6+
interface ToolCallEntry {
7+
name: string;
8+
args: string;
9+
result?: string;
10+
}
11+
12+
/**
13+
* FilesystemComponent demonstrates agent file operations.
14+
*
15+
* The agent can read and write files using tool calls. The sidebar
16+
* shows a real-time log of each file operation as it happens.
17+
*
18+
* Key integration points:
19+
* - `stream.messages()` contains all messages including tool call results
20+
* - `computed()` derives tool call entries from AI messages
21+
* - Tool calls update reactively as the agent performs file operations
22+
*/
823
@Component({
924
selector: 'app-filesystem',
1025
standalone: true,
11-
imports: [ChatDebugComponent],
26+
imports: [LegacyChatComponent],
1227
template: `
13-
<div class="flex h-screen">
14-
<chat-debug [ref]="stream" class="flex-1 min-w-0" />
15-
<aside class="w-72 shrink-0 border-l overflow-y-auto p-4 space-y-2"
16-
style="border-color: var(--chat-border, #333); background: var(--chat-bg, #171717); color: var(--chat-text, #e0e0e0);">
17-
<h3 class="text-xs font-semibold uppercase tracking-wide mb-3"
18-
style="color: var(--chat-text-muted, #777);">File Operations</h3>
19-
@if (fileOps().length === 0) {
20-
<p class="text-sm italic" style="color: var(--chat-text-muted, #777);">No file operations yet. Ask the agent to read or write a file.</p>
21-
}
22-
@for (op of fileOps(); track $index) {
23-
<div class="flex items-start gap-2 text-sm py-1">
24-
<span class="shrink-0">{{ op.name === 'read_file' ? '📖' : '✏️' }}</span>
25-
<span class="font-mono text-xs break-all"
26-
style="color: var(--chat-text, #e0e0e0);">{{ op.path }}</span>
28+
<cp-chat
29+
[messages]="stream.messages()"
30+
[isLoading]="stream.isLoading()"
31+
[error]="stream.error()"
32+
(sendMessage)="send($event)">
33+
<ng-template #sidebar>
34+
<h3 style="font-size: 0.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #1a1a2e;">File Operations</h3>
35+
@for (entry of toolCallEntries(); track $index) {
36+
<div style="display: flex; align-items: flex-start; gap: 8px; padding: 6px 0; font-size: 0.8rem; border-bottom: 1px solid #e5e7eb;">
37+
<span style="flex-shrink: 0; font-size: 1rem; line-height: 1.2;">
38+
{{ entry.name === 'read_file' ? '📖' : '✏️' }}
39+
</span>
40+
<div style="min-width: 0;">
41+
<div style="font-weight: 500; color: #1a1a2e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
42+
{{ getFilePath(entry.args) }}
43+
</div>
44+
<div style="color: #8b8fa3; font-size: 0.75rem; margin-top: 2px;">
45+
{{ entry.name === 'read_file' ? 'read' : 'write' }}
46+
{{ entry.result ? ' · done' : ' · running…' }}
47+
</div>
48+
</div>
2749
</div>
2850
}
29-
</aside>
30-
</div>
51+
@empty {
52+
<p style="color: #8b8fa3; font-size: 0.8rem;">Ask the agent to read or write a file.</p>
53+
}
54+
</ng-template>
55+
</cp-chat>
3156
`,
3257
})
3358
export class FilesystemComponent {
@@ -36,17 +61,29 @@ export class FilesystemComponent {
3661
assistantId: environment.streamingAssistantId,
3762
});
3863

39-
protected readonly fileOps = computed(() => {
40-
const messages = this.stream.messages();
41-
const ops: { name: string; path: string }[] = [];
42-
for (const msg of messages) {
43-
if (!(msg instanceof AIMessage)) continue;
44-
for (const tc of this.stream.getToolCalls(msg)) {
45-
if (tc.call.name === 'read_file' || tc.call.name === 'write_file') {
46-
ops.push({ name: tc.call.name, path: (tc.call.args as Record<string, string>)?.['path'] ?? '' });
64+
toolCallEntries = computed(() => {
65+
const msg = this.stream.messages();
66+
const calls: ToolCallEntry[] = [];
67+
for (const m of msg) {
68+
if ((m as any).tool_calls) {
69+
for (const tc of (m as any).tool_calls) {
70+
calls.push({ name: tc.name, args: JSON.stringify(tc.args), result: tc.output });
4771
}
4872
}
4973
}
50-
return ops;
74+
return calls;
5175
});
76+
77+
getFilePath(args: string): string {
78+
try {
79+
const parsed = JSON.parse(args);
80+
return parsed.path ?? args;
81+
} catch {
82+
return args;
83+
}
84+
}
85+
86+
send(text: string): void {
87+
this.stream.submit({ messages: [{ role: 'human', content: text }] });
88+
}
5289
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
22
* Production environment configuration.
33
*
4-
* Points to the LangGraph Cloud deployment managed by LangSmith.
5-
* The assistantId must match the graph name in langgraph.json.
4+
* Uses relative /api URL — Vercel middleware proxies to LangGraph Cloud
5+
* and injects the x-api-key header server-side.
66
*/
77
export const environment = {
88
production: true,
9-
langGraphApiUrl: 'https://filesystem-2330285f57625bff8654bc026f70a6ae.us.langgraph.app',
9+
langGraphApiUrl: '/api',
1010
streamingAssistantId: 'filesystem',
1111
};

cockpit/deep-agents/filesystem/python/langgraph.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"graphs": {
33
"filesystem": "./src/graph.py:graph"
44
},
5-
"dependencies": ["."],
6-
"python_version": "3.12"
5+
"dependencies": [
6+
"."
7+
],
8+
"python_version": "3.12",
9+
"env": ".env"
710
}
Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,43 @@
1-
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
21
import { Component, computed } from '@angular/core';
3-
import { ChatDebugComponent } from '@cacheplane/chat';
2+
import { LegacyChatComponent } from '@cacheplane/chat';
43
import { streamResource } from '@cacheplane/stream-resource';
54
import { environment } from '../environments/environment';
65

6+
/**
7+
* MemoryComponent demonstrates persistent agent memory across sessions.
8+
*
9+
* The agent extracts facts about the user from each conversation turn
10+
* and stores them in `agent_memory` state. The sidebar shows all learned
11+
* facts in real time as the agent updates its memory.
12+
*
13+
* Key integration points:
14+
* - `stream.value()` contains the agent state including `agent_memory`
15+
* - `computed()` derives key/value pairs for the sidebar
16+
* - Memory entries update reactively as the agent learns new facts
17+
*/
718
@Component({
819
selector: 'app-da-memory',
920
standalone: true,
10-
imports: [ChatDebugComponent],
21+
imports: [LegacyChatComponent],
1122
template: `
12-
<div class="flex h-screen">
13-
<chat-debug [ref]="stream" class="flex-1 min-w-0" />
14-
<aside class="w-72 shrink-0 border-l overflow-y-auto p-4 space-y-2"
15-
style="border-color: var(--chat-border, #333); background: var(--chat-bg, #171717); color: var(--chat-text, #e0e0e0);">
16-
<h3 class="text-xs font-semibold uppercase tracking-wide mb-3"
17-
style="color: var(--chat-text-muted, #777);">Agent Memory</h3>
18-
@if (memoryEntries().length === 0) {
19-
<p class="text-sm italic" style="color: var(--chat-text-muted, #777);">No facts learned yet. Have a conversation to build memory.</p>
20-
}
21-
@for (entry of memoryEntries(); track $index) {
22-
<div class="text-sm py-1">
23-
<span class="font-semibold" style="color: var(--chat-text-muted, #777);">{{ entry[0] }}:</span>
24-
<span class="ml-1" style="color: var(--chat-text, #e0e0e0);">{{ entry[1] }}</span>
23+
<cp-chat
24+
[messages]="stream.messages()"
25+
[isLoading]="stream.isLoading()"
26+
[error]="stream.error()"
27+
(sendMessage)="send($event)">
28+
<ng-template #sidebar>
29+
<h3 style="font-size: 0.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #1a1a2e;">Learned Facts</h3>
30+
@for (entry of memoryEntries(); track entry[0]) {
31+
<div style="padding: 6px 0; font-size: 0.8rem; border-bottom: 1px solid #e5e7eb;">
32+
<div style="font-weight: 600; color: #1a1a2e; margin-bottom: 2px;">{{ entry[0] }}</div>
33+
<div style="color: #555770;">{{ entry[1] }}</div>
2534
</div>
2635
}
27-
</aside>
28-
</div>
36+
@empty {
37+
<p style="color: #8b8fa3; font-size: 0.8rem;">Tell the agent something about yourself to see it remember.</p>
38+
}
39+
</ng-template>
40+
</cp-chat>
2941
`,
3042
})
3143
export class MemoryComponent {
@@ -34,10 +46,12 @@ export class MemoryComponent {
3446
assistantId: environment.streamingAssistantId,
3547
});
3648

37-
protected readonly memoryEntries = computed(() => {
38-
const val = this.stream.value() as Record<string, unknown>;
39-
const mem = val?.['agent_memory'];
40-
if (!mem || typeof mem !== 'object') return [];
41-
return Object.entries(mem as Record<string, string>);
49+
memoryEntries = computed(() => {
50+
const val = this.stream.value() as { agent_memory?: Record<string, string> } | undefined;
51+
return Object.entries(val?.agent_memory ?? {});
4252
});
53+
54+
send(text: string): void {
55+
this.stream.submit({ messages: [{ role: 'human', content: text }] });
56+
}
4357
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
22
* Production environment configuration.
33
*
4-
* Points to the LangGraph Cloud deployment managed by LangSmith.
5-
* The assistantId must match the graph name in langgraph.json.
4+
* Uses relative /api URL — Vercel middleware proxies to LangGraph Cloud
5+
* and injects the x-api-key header server-side.
66
*/
77
export const environment = {
88
production: true,
9-
langGraphApiUrl: 'https://da-memory-15f767adfa6f5cd48bd45a0fa4db29b5.us.langgraph.app',
9+
langGraphApiUrl: '/api',
1010
streamingAssistantId: 'da-memory',
1111
};

0 commit comments

Comments
 (0)