diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6783ac35..27c3de43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -277,6 +277,13 @@ jobs: - { angular: cockpit-chat-theming-angular, python: cockpit/chat/theming/python } - { angular: cockpit-chat-generative-ui-angular, python: cockpit/chat/generative-ui/python } - { angular: cockpit-chat-a2ui-angular, python: cockpit/chat/a2ui/python } + - { angular: cockpit-langgraph-persistence-angular, python: cockpit/langgraph/persistence/python } + - { angular: cockpit-langgraph-interrupts-angular, python: cockpit/langgraph/interrupts/python } + - { angular: cockpit-langgraph-memory-angular, python: cockpit/langgraph/memory/python } + - { angular: cockpit-langgraph-durable-execution-angular, python: cockpit/langgraph/durable-execution/python } + - { angular: cockpit-langgraph-subgraphs-angular, python: cockpit/langgraph/subgraphs/python } + - { angular: cockpit-langgraph-time-travel-angular, python: cockpit/langgraph/time-travel/python } + - { angular: cockpit-langgraph-deployment-runtime-angular, python: cockpit/langgraph/deployment-runtime/python } steps: - uses: actions/checkout@v6.0.2 - uses: actions/setup-node@v6.3.0 diff --git a/apps/cockpit/scripts/capability-registry.ts b/apps/cockpit/scripts/capability-registry.ts index 147e3aa0..077ba68a 100644 --- a/apps/cockpit/scripts/capability-registry.ts +++ b/apps/cockpit/scripts/capability-registry.ts @@ -16,14 +16,14 @@ export interface Capability { } export const capabilities: readonly Capability[] = [ - { id: 'streaming', product: 'langgraph', topic: 'streaming', angularProject: 'cockpit-langgraph-streaming-angular', port: 4300, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'streaming' }, - { id: 'persistence', product: 'langgraph', topic: 'persistence', angularProject: 'cockpit-langgraph-persistence-angular', port: 4301, pythonDir: 'cockpit/langgraph/persistence/python', graphName: 'persistence' }, - { id: 'interrupts', product: 'langgraph', topic: 'interrupts', angularProject: 'cockpit-langgraph-interrupts-angular', port: 4302, pythonDir: 'cockpit/langgraph/interrupts/python', graphName: 'interrupts' }, - { id: 'memory', product: 'langgraph', topic: 'memory', angularProject: 'cockpit-langgraph-memory-angular', port: 4303, pythonDir: 'cockpit/langgraph/memory/python', graphName: 'memory' }, - { id: 'durable-execution', product: 'langgraph', topic: 'durable-execution', angularProject: 'cockpit-langgraph-durable-execution-angular', port: 4304, pythonDir: 'cockpit/langgraph/durable-execution/python', graphName: 'durable-execution' }, - { id: 'subgraphs', product: 'langgraph', topic: 'subgraphs', angularProject: 'cockpit-langgraph-subgraphs-angular', port: 4305, pythonDir: 'cockpit/langgraph/subgraphs/python', graphName: 'subgraphs' }, - { id: 'time-travel', product: 'langgraph', topic: 'time-travel', angularProject: 'cockpit-langgraph-time-travel-angular', port: 4306, pythonDir: 'cockpit/langgraph/time-travel/python', graphName: 'time-travel' }, - { id: 'deployment-runtime', product: 'langgraph', topic: 'deployment-runtime', angularProject: 'cockpit-langgraph-deployment-runtime-angular', port: 4307, pythonDir: 'cockpit/langgraph/deployment-runtime/python', graphName: 'deployment-runtime' }, + { id: 'streaming', product: 'langgraph', topic: 'streaming', angularProject: 'cockpit-langgraph-streaming-angular', port: 4300, pythonPort: 5300, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'streaming' }, + { id: 'persistence', product: 'langgraph', topic: 'persistence', angularProject: 'cockpit-langgraph-persistence-angular', port: 4301, pythonPort: 5301, pythonDir: 'cockpit/langgraph/persistence/python', graphName: 'persistence' }, + { id: 'interrupts', product: 'langgraph', topic: 'interrupts', angularProject: 'cockpit-langgraph-interrupts-angular', port: 4302, pythonPort: 5302, pythonDir: 'cockpit/langgraph/interrupts/python', graphName: 'interrupts' }, + { id: 'memory', product: 'langgraph', topic: 'memory', angularProject: 'cockpit-langgraph-memory-angular', port: 4303, pythonPort: 5303, pythonDir: 'cockpit/langgraph/memory/python', graphName: 'memory' }, + { id: 'durable-execution', product: 'langgraph', topic: 'durable-execution', angularProject: 'cockpit-langgraph-durable-execution-angular', port: 4304, pythonPort: 5304, pythonDir: 'cockpit/langgraph/durable-execution/python', graphName: 'durable-execution' }, + { id: 'subgraphs', product: 'langgraph', topic: 'subgraphs', angularProject: 'cockpit-langgraph-subgraphs-angular', port: 4305, pythonPort: 5305, pythonDir: 'cockpit/langgraph/subgraphs/python', graphName: 'subgraphs' }, + { id: 'time-travel', product: 'langgraph', topic: 'time-travel', angularProject: 'cockpit-langgraph-time-travel-angular', port: 4306, pythonPort: 5306, pythonDir: 'cockpit/langgraph/time-travel/python', graphName: 'time-travel' }, + { id: 'deployment-runtime', product: 'langgraph', topic: 'deployment-runtime', angularProject: 'cockpit-langgraph-deployment-runtime-angular', port: 4307, pythonPort: 5307, pythonDir: 'cockpit/langgraph/deployment-runtime/python', graphName: 'deployment-runtime' }, { id: 'da-planning', product: 'deep-agents', topic: 'planning', angularProject: 'cockpit-deep-agents-planning-angular', port: 4310, pythonDir: 'cockpit/deep-agents/planning/python', graphName: 'da-planning' }, { id: 'da-filesystem', product: 'deep-agents', topic: 'filesystem', angularProject: 'cockpit-deep-agents-filesystem-angular', port: 4311, pythonDir: 'cockpit/deep-agents/filesystem/python', graphName: 'da-filesystem' }, { id: 'da-subagents', product: 'deep-agents', topic: 'subagents', angularProject: 'cockpit-deep-agents-subagents-angular', port: 4312, pythonDir: 'cockpit/deep-agents/subagents/python', graphName: 'subagents' }, diff --git a/cockpit/langgraph/deployment-runtime/angular/e2e/deployment-runtime.spec.ts b/cockpit/langgraph/deployment-runtime/angular/e2e/deployment-runtime.spec.ts new file mode 100644 index 00000000..a879ce70 --- /dev/null +++ b/cockpit/langgraph/deployment-runtime/angular/e2e/deployment-runtime.spec.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('deployment-runtime: hello prompt produces assistant turn', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, 'Hello'); + // Smoke: backend booted, aimock replayed fixture, assistant bubble + // finalized (data-streaming="false") and is present in the DOM. + await expect(bubble).toBeVisible(); +}); diff --git a/cockpit/langgraph/deployment-runtime/angular/e2e/fixtures/deployment-runtime.json b/cockpit/langgraph/deployment-runtime/angular/e2e/fixtures/deployment-runtime.json new file mode 100644 index 00000000..8766ff23 --- /dev/null +++ b/cockpit/langgraph/deployment-runtime/angular/e2e/fixtures/deployment-runtime.json @@ -0,0 +1,12 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Hello" + }, + "response": { + "content": "Hello! How can I help you today?" + } + } + ] +} \ No newline at end of file diff --git a/cockpit/langgraph/deployment-runtime/angular/e2e/global-setup-impl.ts b/cockpit/langgraph/deployment-runtime/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..930ed385 --- /dev/null +++ b/cockpit/langgraph/deployment-runtime/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/langgraph/deployment-runtime/python', + langgraphPort: 5307, + angularProject: 'cockpit-langgraph-deployment-runtime-angular', + angularPort: 4307, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/langgraph/deployment-runtime/angular/e2e/playwright.config.ts b/cockpit/langgraph/deployment-runtime/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..cee41f1a --- /dev/null +++ b/cockpit/langgraph/deployment-runtime/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4307', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/langgraph/deployment-runtime/angular/e2e/tsconfig.json b/cockpit/langgraph/deployment-runtime/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/langgraph/deployment-runtime/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/langgraph/deployment-runtime/angular/project.json b/cockpit/langgraph/deployment-runtime/angular/project.json index bd9809d8..810d693c 100644 --- a/cockpit/langgraph/deployment-runtime/angular/project.json +++ b/cockpit/langgraph/deployment-runtime/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/langgraph/deployment-runtime/angular", "command": "npx tsx -e \"import { langgraphDeploymentRuntimeAngularModule } from './src/index.ts'; const module = langgraphDeploymentRuntimeAngularModule; if (module.id !== 'langgraph-deployment-runtime-angular' || module.title !== 'LangGraph Deployment & Runtime (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/langgraph/deployment-runtime/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/langgraph/deployment-runtime/angular/proxy.conf.json b/cockpit/langgraph/deployment-runtime/angular/proxy.conf.json index 8523362d..73b38f86 100644 --- a/cockpit/langgraph/deployment-runtime/angular/proxy.conf.json +++ b/cockpit/langgraph/deployment-runtime/angular/proxy.conf.json @@ -1,9 +1,11 @@ { "/api": { - "target": "http://localhost:8123", + "target": "http://localhost:5307", "secure": false, "changeOrigin": true, - "pathRewrite": { "^/api": "" }, + "pathRewrite": { + "^/api": "" + }, "ws": true } } diff --git a/cockpit/langgraph/durable-execution/angular/e2e/durable-execution.spec.ts b/cockpit/langgraph/durable-execution/angular/e2e/durable-execution.spec.ts new file mode 100644 index 00000000..8e278948 --- /dev/null +++ b/cockpit/langgraph/durable-execution/angular/e2e/durable-execution.spec.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('durable-execution: hello prompt produces assistant turn', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, 'Hello'); + // Smoke: backend booted, aimock replayed fixture, assistant bubble + // finalized (data-streaming="false") and is present in the DOM. + await expect(bubble).toBeVisible(); +}); diff --git a/cockpit/langgraph/durable-execution/angular/e2e/fixtures/durable-execution.json b/cockpit/langgraph/durable-execution/angular/e2e/fixtures/durable-execution.json new file mode 100644 index 00000000..8c8b3198 --- /dev/null +++ b/cockpit/langgraph/durable-execution/angular/e2e/fixtures/durable-execution.json @@ -0,0 +1,12 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Hello" + }, + "response": { + "content": "The user sent a simple greeting (\"Hello\"), so the immediate goal is to acknowledge the greeting and invite them to continue (e.g., ask how I can help or what they'd like to discuss)." + } + } + ] +} \ No newline at end of file diff --git a/cockpit/langgraph/durable-execution/angular/e2e/global-setup-impl.ts b/cockpit/langgraph/durable-execution/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..6ada3416 --- /dev/null +++ b/cockpit/langgraph/durable-execution/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/langgraph/durable-execution/python', + langgraphPort: 5304, + angularProject: 'cockpit-langgraph-durable-execution-angular', + angularPort: 4304, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/langgraph/durable-execution/angular/e2e/playwright.config.ts b/cockpit/langgraph/durable-execution/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..52f41a3d --- /dev/null +++ b/cockpit/langgraph/durable-execution/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4304', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/langgraph/durable-execution/angular/e2e/tsconfig.json b/cockpit/langgraph/durable-execution/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/langgraph/durable-execution/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/langgraph/durable-execution/angular/project.json b/cockpit/langgraph/durable-execution/angular/project.json index 84185da1..b5a0856f 100644 --- a/cockpit/langgraph/durable-execution/angular/project.json +++ b/cockpit/langgraph/durable-execution/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/langgraph/durable-execution/angular", "command": "npx tsx -e \"import { langgraphDurableExecutionAngularModule } from './src/index.ts'; const module = langgraphDurableExecutionAngularModule; if (module.id !== 'langgraph-durable-execution-angular' || module.title !== 'LangGraph Durable Execution (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/langgraph/durable-execution/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/langgraph/durable-execution/angular/proxy.conf.json b/cockpit/langgraph/durable-execution/angular/proxy.conf.json index 8523362d..3635c645 100644 --- a/cockpit/langgraph/durable-execution/angular/proxy.conf.json +++ b/cockpit/langgraph/durable-execution/angular/proxy.conf.json @@ -1,9 +1,11 @@ { "/api": { - "target": "http://localhost:8123", + "target": "http://localhost:5304", "secure": false, "changeOrigin": true, - "pathRewrite": { "^/api": "" }, + "pathRewrite": { + "^/api": "" + }, "ws": true } } diff --git a/cockpit/langgraph/interrupts/angular/e2e/fixtures/interrupts.json b/cockpit/langgraph/interrupts/angular/e2e/fixtures/interrupts.json new file mode 100644 index 00000000..518cc809 --- /dev/null +++ b/cockpit/langgraph/interrupts/angular/e2e/fixtures/interrupts.json @@ -0,0 +1,12 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Hello" + }, + "response": { + "content": "Hello! How can I help you today? Please approve this response to continue." + } + } + ] +} \ No newline at end of file diff --git a/cockpit/langgraph/interrupts/angular/e2e/global-setup-impl.ts b/cockpit/langgraph/interrupts/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..9623030e --- /dev/null +++ b/cockpit/langgraph/interrupts/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/langgraph/interrupts/python', + langgraphPort: 5302, + angularProject: 'cockpit-langgraph-interrupts-angular', + angularPort: 4302, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/langgraph/interrupts/angular/e2e/interrupts.spec.ts b/cockpit/langgraph/interrupts/angular/e2e/interrupts.spec.ts new file mode 100644 index 00000000..1676090c --- /dev/null +++ b/cockpit/langgraph/interrupts/angular/e2e/interrupts.spec.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('interrupts: hello prompt produces assistant turn', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, 'Hello'); + // Smoke: backend booted, aimock replayed fixture, assistant bubble + // finalized (data-streaming="false") and is present in the DOM. + await expect(bubble).toBeVisible(); +}); diff --git a/cockpit/langgraph/interrupts/angular/e2e/playwright.config.ts b/cockpit/langgraph/interrupts/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..611ac2c4 --- /dev/null +++ b/cockpit/langgraph/interrupts/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4302', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/langgraph/interrupts/angular/e2e/tsconfig.json b/cockpit/langgraph/interrupts/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/langgraph/interrupts/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/langgraph/interrupts/angular/project.json b/cockpit/langgraph/interrupts/angular/project.json index 20e4a9c1..9f5b88de 100644 --- a/cockpit/langgraph/interrupts/angular/project.json +++ b/cockpit/langgraph/interrupts/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/langgraph/interrupts/angular", "command": "npx tsx -e \"import { langgraphInterruptsAngularModule } from './src/index.ts'; const module = langgraphInterruptsAngularModule; if (module.id !== 'langgraph-interrupts-angular' || module.title !== 'LangGraph Interrupts (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/langgraph/interrupts/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/langgraph/interrupts/angular/proxy.conf.json b/cockpit/langgraph/interrupts/angular/proxy.conf.json index 8523362d..1e81d1e5 100644 --- a/cockpit/langgraph/interrupts/angular/proxy.conf.json +++ b/cockpit/langgraph/interrupts/angular/proxy.conf.json @@ -1,9 +1,11 @@ { "/api": { - "target": "http://localhost:8123", + "target": "http://localhost:5302", "secure": false, "changeOrigin": true, - "pathRewrite": { "^/api": "" }, + "pathRewrite": { + "^/api": "" + }, "ws": true } } diff --git a/cockpit/langgraph/memory/angular/e2e/fixtures/memory.json b/cockpit/langgraph/memory/angular/e2e/fixtures/memory.json new file mode 100644 index 00000000..2ee8f223 --- /dev/null +++ b/cockpit/langgraph/memory/angular/e2e/fixtures/memory.json @@ -0,0 +1,18 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Hello" + }, + "response": { + "content": "Hi \u2014 nice to meet you! How can I help today? If you like, tell me your name or anything you want me to remember for future chats." + } + }, + { + "match": {}, + "response": { + "content": "{}" + } + } + ] +} \ No newline at end of file diff --git a/cockpit/langgraph/memory/angular/e2e/global-setup-impl.ts b/cockpit/langgraph/memory/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..175d6275 --- /dev/null +++ b/cockpit/langgraph/memory/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/langgraph/memory/python', + langgraphPort: 5303, + angularProject: 'cockpit-langgraph-memory-angular', + angularPort: 4303, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/langgraph/memory/angular/e2e/memory.spec.ts b/cockpit/langgraph/memory/angular/e2e/memory.spec.ts new file mode 100644 index 00000000..cbb237ef --- /dev/null +++ b/cockpit/langgraph/memory/angular/e2e/memory.spec.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('memory: hello prompt produces assistant turn', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, 'Hello'); + // Smoke: backend booted, aimock replayed fixture, assistant bubble + // finalized (data-streaming="false") and is present in the DOM. + await expect(bubble).toBeVisible(); +}); diff --git a/cockpit/langgraph/memory/angular/e2e/playwright.config.ts b/cockpit/langgraph/memory/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..c9ac8499 --- /dev/null +++ b/cockpit/langgraph/memory/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4303', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/langgraph/memory/angular/e2e/tsconfig.json b/cockpit/langgraph/memory/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/langgraph/memory/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/langgraph/memory/angular/project.json b/cockpit/langgraph/memory/angular/project.json index a097b991..4ca8d9ab 100644 --- a/cockpit/langgraph/memory/angular/project.json +++ b/cockpit/langgraph/memory/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/langgraph/memory/angular", "command": "npx tsx -e \"import { langgraphMemoryAngularModule } from './src/index.ts'; const module = langgraphMemoryAngularModule; if (module.id !== 'langgraph-memory-angular' || module.title !== 'LangGraph Memory (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/langgraph/memory/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/langgraph/memory/angular/proxy.conf.json b/cockpit/langgraph/memory/angular/proxy.conf.json index 8523362d..2ccccc5c 100644 --- a/cockpit/langgraph/memory/angular/proxy.conf.json +++ b/cockpit/langgraph/memory/angular/proxy.conf.json @@ -1,9 +1,11 @@ { "/api": { - "target": "http://localhost:8123", + "target": "http://localhost:5303", "secure": false, "changeOrigin": true, - "pathRewrite": { "^/api": "" }, + "pathRewrite": { + "^/api": "" + }, "ws": true } } diff --git a/cockpit/langgraph/persistence/angular/e2e/fixtures/persistence.json b/cockpit/langgraph/persistence/angular/e2e/fixtures/persistence.json new file mode 100644 index 00000000..0d91089d --- /dev/null +++ b/cockpit/langgraph/persistence/angular/e2e/fixtures/persistence.json @@ -0,0 +1,12 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Hello" + }, + "response": { + "content": "Hi \u2014 how can I help you today?" + } + } + ] +} \ No newline at end of file diff --git a/cockpit/langgraph/persistence/angular/e2e/global-setup-impl.ts b/cockpit/langgraph/persistence/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..bec93349 --- /dev/null +++ b/cockpit/langgraph/persistence/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/langgraph/persistence/python', + langgraphPort: 5301, + angularProject: 'cockpit-langgraph-persistence-angular', + angularPort: 4301, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/langgraph/persistence/angular/e2e/persistence.spec.ts b/cockpit/langgraph/persistence/angular/e2e/persistence.spec.ts new file mode 100644 index 00000000..1c7c9342 --- /dev/null +++ b/cockpit/langgraph/persistence/angular/e2e/persistence.spec.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('persistence: hello prompt produces assistant turn', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, 'Hello'); + // Smoke: backend booted, aimock replayed fixture, assistant bubble + // finalized (data-streaming="false") and is present in the DOM. + await expect(bubble).toBeVisible(); +}); diff --git a/cockpit/langgraph/persistence/angular/e2e/playwright.config.ts b/cockpit/langgraph/persistence/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..0f924be4 --- /dev/null +++ b/cockpit/langgraph/persistence/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4301', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/langgraph/persistence/angular/e2e/tsconfig.json b/cockpit/langgraph/persistence/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/langgraph/persistence/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/langgraph/persistence/angular/project.json b/cockpit/langgraph/persistence/angular/project.json index 32cee184..f2889f8c 100644 --- a/cockpit/langgraph/persistence/angular/project.json +++ b/cockpit/langgraph/persistence/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/langgraph/persistence/angular", "command": "npx tsx -e \"import { langgraphPersistenceAngularModule } from './src/index.ts'; const module = langgraphPersistenceAngularModule; if (module.id !== 'langgraph-persistence-angular' || module.title !== 'LangGraph Persistence (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/langgraph/persistence/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/langgraph/persistence/angular/proxy.conf.json b/cockpit/langgraph/persistence/angular/proxy.conf.json index 8523362d..dbab65e0 100644 --- a/cockpit/langgraph/persistence/angular/proxy.conf.json +++ b/cockpit/langgraph/persistence/angular/proxy.conf.json @@ -1,9 +1,11 @@ { "/api": { - "target": "http://localhost:8123", + "target": "http://localhost:5301", "secure": false, "changeOrigin": true, - "pathRewrite": { "^/api": "" }, + "pathRewrite": { + "^/api": "" + }, "ws": true } } diff --git a/cockpit/langgraph/streaming/angular/e2e/global-setup-impl.ts b/cockpit/langgraph/streaming/angular/e2e/global-setup-impl.ts index 76fb474d..66e5fcbe 100644 --- a/cockpit/langgraph/streaming/angular/e2e/global-setup-impl.ts +++ b/cockpit/langgraph/streaming/angular/e2e/global-setup-impl.ts @@ -4,6 +4,7 @@ import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; export default createGlobalSetup({ langgraphCwd: 'cockpit/langgraph/streaming/python', + langgraphPort: 5300, angularProject: 'cockpit-langgraph-streaming-angular', angularPort: 4300, fixturesDir: resolve(__dirname, 'fixtures'), diff --git a/cockpit/langgraph/streaming/angular/proxy.conf.json b/cockpit/langgraph/streaming/angular/proxy.conf.json index 8523362d..65d40b4b 100644 --- a/cockpit/langgraph/streaming/angular/proxy.conf.json +++ b/cockpit/langgraph/streaming/angular/proxy.conf.json @@ -1,9 +1,11 @@ { "/api": { - "target": "http://localhost:8123", + "target": "http://localhost:5300", "secure": false, "changeOrigin": true, - "pathRewrite": { "^/api": "" }, + "pathRewrite": { + "^/api": "" + }, "ws": true } } diff --git a/cockpit/langgraph/subgraphs/angular/e2e/fixtures/subgraphs.json b/cockpit/langgraph/subgraphs/angular/e2e/fixtures/subgraphs.json new file mode 100644 index 00000000..9622bf64 --- /dev/null +++ b/cockpit/langgraph/subgraphs/angular/e2e/fixtures/subgraphs.json @@ -0,0 +1,12 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Hello" + }, + "response": { + "content": "Hello \u2014 nice to meet you. What would you like help with today?\n\nTell me the specific question or topic and any preferences (depth: quick summary vs deep dive, date range, types of sources, format: bullet list, tutorial, pros/cons, citations). Example: \u201cCompare CRISPR vs base editors for somatic therapy, include clinical trials since 2020 and cite sources.\u201d\n\nOnce you reply I\u2019ll prepare and run a focused research query and return the findings." + } + } + ] +} \ No newline at end of file diff --git a/cockpit/langgraph/subgraphs/angular/e2e/global-setup-impl.ts b/cockpit/langgraph/subgraphs/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..3a1a1558 --- /dev/null +++ b/cockpit/langgraph/subgraphs/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/langgraph/subgraphs/python', + langgraphPort: 5305, + angularProject: 'cockpit-langgraph-subgraphs-angular', + angularPort: 4305, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/langgraph/subgraphs/angular/e2e/playwright.config.ts b/cockpit/langgraph/subgraphs/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..1c8f5a8e --- /dev/null +++ b/cockpit/langgraph/subgraphs/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4305', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/langgraph/subgraphs/angular/e2e/subgraphs.spec.ts b/cockpit/langgraph/subgraphs/angular/e2e/subgraphs.spec.ts new file mode 100644 index 00000000..7609f17d --- /dev/null +++ b/cockpit/langgraph/subgraphs/angular/e2e/subgraphs.spec.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('subgraphs: hello prompt produces assistant turn', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, 'Hello'); + // Smoke: backend booted, aimock replayed fixture, assistant bubble + // finalized (data-streaming="false") and is present in the DOM. + await expect(bubble).toBeVisible(); +}); diff --git a/cockpit/langgraph/subgraphs/angular/e2e/tsconfig.json b/cockpit/langgraph/subgraphs/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/langgraph/subgraphs/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/langgraph/subgraphs/angular/project.json b/cockpit/langgraph/subgraphs/angular/project.json index 4171db44..66926438 100644 --- a/cockpit/langgraph/subgraphs/angular/project.json +++ b/cockpit/langgraph/subgraphs/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/langgraph/subgraphs/angular", "command": "npx tsx -e \"import { langgraphSubgraphsAngularModule } from './src/index.ts'; const module = langgraphSubgraphsAngularModule; if (module.id !== 'langgraph-subgraphs-angular' || module.title !== 'LangGraph Subgraphs (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/langgraph/subgraphs/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/langgraph/subgraphs/angular/proxy.conf.json b/cockpit/langgraph/subgraphs/angular/proxy.conf.json index 8523362d..47e38550 100644 --- a/cockpit/langgraph/subgraphs/angular/proxy.conf.json +++ b/cockpit/langgraph/subgraphs/angular/proxy.conf.json @@ -1,9 +1,11 @@ { "/api": { - "target": "http://localhost:8123", + "target": "http://localhost:5305", "secure": false, "changeOrigin": true, - "pathRewrite": { "^/api": "" }, + "pathRewrite": { + "^/api": "" + }, "ws": true } } diff --git a/cockpit/langgraph/time-travel/angular/e2e/fixtures/time-travel.json b/cockpit/langgraph/time-travel/angular/e2e/fixtures/time-travel.json new file mode 100644 index 00000000..b72a2018 --- /dev/null +++ b/cockpit/langgraph/time-travel/angular/e2e/fixtures/time-travel.json @@ -0,0 +1,12 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Hello" + }, + "response": { + "content": "Hi! How can I help you today?\n\nQuick note: every response here is saved as a checkpoint snapshot. If you want to explore a different path later, you can inspect the conversation history and branch from any previous checkpoint using stream.setBranch(checkpointId) to create an alternate timeline." + } + } + ] +} \ No newline at end of file diff --git a/cockpit/langgraph/time-travel/angular/e2e/global-setup-impl.ts b/cockpit/langgraph/time-travel/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..c4f98881 --- /dev/null +++ b/cockpit/langgraph/time-travel/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/langgraph/time-travel/python', + langgraphPort: 5306, + angularProject: 'cockpit-langgraph-time-travel-angular', + angularPort: 4306, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/langgraph/time-travel/angular/e2e/playwright.config.ts b/cockpit/langgraph/time-travel/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..dfb767b2 --- /dev/null +++ b/cockpit/langgraph/time-travel/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4306', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/langgraph/time-travel/angular/e2e/time-travel.spec.ts b/cockpit/langgraph/time-travel/angular/e2e/time-travel.spec.ts new file mode 100644 index 00000000..2b6b579e --- /dev/null +++ b/cockpit/langgraph/time-travel/angular/e2e/time-travel.spec.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('time-travel: hello prompt produces assistant turn', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, 'Hello'); + // Smoke: backend booted, aimock replayed fixture, assistant bubble + // finalized (data-streaming="false") and is present in the DOM. + await expect(bubble).toBeVisible(); +}); diff --git a/cockpit/langgraph/time-travel/angular/e2e/tsconfig.json b/cockpit/langgraph/time-travel/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/langgraph/time-travel/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/langgraph/time-travel/angular/project.json b/cockpit/langgraph/time-travel/angular/project.json index c4a763db..4e287272 100644 --- a/cockpit/langgraph/time-travel/angular/project.json +++ b/cockpit/langgraph/time-travel/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/langgraph/time-travel/angular", "command": "npx tsx -e \"import { langgraphTimeTravelAngularModule } from './src/index.ts'; const module = langgraphTimeTravelAngularModule; if (module.id !== 'langgraph-time-travel-angular' || module.title !== 'LangGraph Time Travel (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/langgraph/time-travel/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/langgraph/time-travel/angular/proxy.conf.json b/cockpit/langgraph/time-travel/angular/proxy.conf.json index 8523362d..78ddc28b 100644 --- a/cockpit/langgraph/time-travel/angular/proxy.conf.json +++ b/cockpit/langgraph/time-travel/angular/proxy.conf.json @@ -1,9 +1,11 @@ { "/api": { - "target": "http://localhost:8123", + "target": "http://localhost:5306", "secure": false, "changeOrigin": true, - "pathRewrite": { "^/api": "" }, + "pathRewrite": { + "^/api": "" + }, "ws": true } } diff --git a/scripts/record-aimock-cap.sh b/scripts/record-aimock-cap.sh new file mode 100755 index 00000000..1d714dec --- /dev/null +++ b/scripts/record-aimock-cap.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT +# +# Generic aimock fixture recorder for a single cockpit cap. +# Reads cap metadata from apps/cockpit/scripts/capability-registry.ts via tsx, +# drives one or more prompts through aimock --record mode, merges captured +# fixtures into the cap's e2e/fixtures/.json. +# +# Usage: +# OPENAI_API_KEY=sk-... bash scripts/record-aimock-cap.sh "" ["" ...] +# +# Example: +# OPENAI_API_KEY=sk-... bash scripts/record-aimock-cap.sh persistence "Hello" +# +# Throwaway tool. Delete after the langgraph batch lands. +set -euo pipefail + +CAP_ID="${1:?cap id required as first arg}" +shift +if [[ $# -eq 0 ]]; then + echo "Error: at least one prompt required" >&2 + exit 1 +fi +PROMPTS=("$@") + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +# Look up cap metadata via the registry. +read -r CAP_PRODUCT CAP_TOPIC CAP_GRAPH CAP_PORT CAP_PYPORT CAP_PYDIR < <(npx tsx -e " +import { capabilities } from './apps/cockpit/scripts/capability-registry'; +const c = capabilities.find(x => x.id === '$CAP_ID'); +if (!c) { console.error('cap not found: $CAP_ID'); process.exit(1); } +if (!c.pythonDir || c.pythonPort === undefined) { console.error('cap missing pythonDir/pythonPort'); process.exit(1); } +console.log(c.product, c.topic, c.graphName, c.port, c.pythonPort, c.pythonDir); +") + +if [[ -z "${OPENAI_API_KEY:-}" ]]; then + for env_path in examples/chat/python/.env "$CAP_PYDIR/.env"; do + if [[ -f "$env_path" ]]; then + set -a; source "$env_path"; set +a + break + fi + done +fi +if [[ -z "${OPENAI_API_KEY:-}" ]]; then + echo "OPENAI_API_KEY not set (in env or examples/chat/python/.env)" >&2 + exit 1 +fi + +AIMOCK_PORT=$((19000 + RANDOM % 900)) +LANGGRAPH_PORT="$CAP_PYPORT" +FIXTURE_OUT="cockpit/$CAP_PRODUCT/$CAP_TOPIC/angular/e2e/fixtures/${CAP_ID}.json" +RECORD_DIR="$(pwd)/cockpit/$CAP_PRODUCT/$CAP_TOPIC/angular/e2e/fixtures/.staging" +rm -rf "$RECORD_DIR" +mkdir -p "$RECORD_DIR" "$(dirname "$FIXTURE_OUT")" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +if [[ -f "examples/chat/python/.env" && ! -f "$CAP_PYDIR/.env" ]]; then + cp examples/chat/python/.env "$CAP_PYDIR/.env" +fi + +# Free the ports if anything still listens (prior aborted record). +for p in "$AIMOCK_PORT" "$LANGGRAPH_PORT"; do + pid=$(lsof -ti:$p 2>/dev/null || true) + if [[ -n "$pid" ]]; then + kill -9 $pid 2>/dev/null || true + sleep 0.5 + fi +done + +echo "[record] starting aimock --record on :$AIMOCK_PORT for $CAP_ID" +npx -y -p @copilotkit/aimock llmock \ + --port "$AIMOCK_PORT" \ + --record \ + --provider-openai https://api.openai.com \ + --fixtures "$RECORD_DIR" \ + --chunk-size 4096 \ + > "$TMP_DIR/aimock.log" 2>&1 & +AIMOCK_PID=$! + +cleanup() { + if [[ -n "${LG_PID:-}" ]]; then + pkill -P "$LG_PID" 2>/dev/null || true + kill "$LG_PID" 2>/dev/null || true + fi + kill "$AIMOCK_PID" 2>/dev/null || true + wait 2>/dev/null || true + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +for _ in {1..30}; do + if curl -sf "http://127.0.0.1:$AIMOCK_PORT/health" > /dev/null 2>&1 \ + || curl -sf "http://127.0.0.1:$AIMOCK_PORT/" > /dev/null 2>&1; then break; fi + sleep 1 +done +echo "[record] aimock ready" + +echo "[record] starting langgraph dev on :$LANGGRAPH_PORT" +RUN_PREFIX="" +if command -v setsid >/dev/null 2>&1; then RUN_PREFIX="setsid"; fi +( + cd "$CAP_PYDIR" + OPENAI_BASE_URL="http://127.0.0.1:$AIMOCK_PORT/v1" OPENAI_API_KEY="$OPENAI_API_KEY" \ + exec $RUN_PREFIX uv run langgraph dev --port "$LANGGRAPH_PORT" --no-browser +) > "$TMP_DIR/langgraph.log" 2>&1 & +LG_PID=$! + +for _ in {1..60}; do + if curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/ok" > /dev/null; then break; fi + sleep 1 +done +if ! curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/ok" > /dev/null; then + echo "[record] langgraph failed to start; tail of log:" >&2 + tail -30 "$TMP_DIR/langgraph.log" >&2 + exit 2 +fi +echo "[record] langgraph ready (graph_id=$CAP_GRAPH)" + +drive_prompt() { + local prompt="$1" + echo "[record][$prompt] thread + run" + local thread run + thread=$(curl -sf -X POST "http://127.0.0.1:$LANGGRAPH_PORT/threads" \ + -H 'content-type: application/json' -d '{}' \ + | python3 -c 'import sys,json; print(json.load(sys.stdin)["thread_id"])') + run=$(curl -sf -X POST "http://127.0.0.1:$LANGGRAPH_PORT/threads/$thread/runs" \ + -H 'content-type: application/json' \ + -d "{\"assistant_id\": \"$CAP_GRAPH\", \"input\": {\"messages\": [{\"role\": \"user\", \"content\": \"$prompt\"}]}}" \ + | python3 -c 'import sys,json; print(json.load(sys.stdin)["run_id"])') + local status="" + for _ in {1..120}; do + status=$(curl -sf "http://127.0.0.1:$LANGGRAPH_PORT/threads/$thread/runs/$run" \ + | python3 -c 'import sys,json; print(json.load(sys.stdin).get("status",""))') + case "$status" in + success|error|timeout|interrupted) break ;; + esac + sleep 2 + done + if [[ "$status" != "success" && "$status" != "interrupted" ]]; then + echo "[record][$prompt] run reached unexpected status=$status" >&2 + tail -40 "$TMP_DIR/langgraph.log" >&2 + exit 3 + fi + echo "[record][$prompt] status=$status" +} + +for p in "${PROMPTS[@]}"; do + drive_prompt "$p" +done + +sleep 2 + +RECORDED_DIR="$RECORD_DIR/recorded" +if [[ ! -d "$RECORDED_DIR" ]]; then + echo "[record] no recorded fixtures dir at $RECORDED_DIR" >&2 + tail -40 "$TMP_DIR/aimock.log" >&2 + exit 5 +fi +N=$(find "$RECORDED_DIR" -name "*.json" | wc -l | tr -d ' ') +echo "[record] $N recorded files in $RECORDED_DIR" + +python3 - <