mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-10-06 12:37:33 +00:00
Merge branch 'main' into responses_object
This commit is contained in:
commit
845e405cd0
26 changed files with 2118 additions and 37 deletions
|
@ -36,7 +36,7 @@ runs:
|
||||||
- name: Run Integration Tests
|
- name: Run Integration Tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
./scripts/integration-tests.sh \
|
uv run --no-sync ./scripts/integration-tests.sh \
|
||||||
--stack-config '${{ inputs.stack-config }}' \
|
--stack-config '${{ inputs.stack-config }}' \
|
||||||
--provider '${{ inputs.provider }}' \
|
--provider '${{ inputs.provider }}' \
|
||||||
--test-subdirs '${{ inputs.test-subdirs }}' \
|
--test-subdirs '${{ inputs.test-subdirs }}' \
|
||||||
|
|
9
.github/actions/setup-runner/action.yml
vendored
9
.github/actions/setup-runner/action.yml
vendored
|
@ -16,14 +16,16 @@ runs:
|
||||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
|
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
activate-environment: true
|
|
||||||
version: 0.7.6
|
version: 0.7.6
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
echo "Updating project dependencies via uv sync"
|
||||||
uv sync --all-groups
|
uv sync --all-groups
|
||||||
uv pip install ollama faiss-cpu
|
|
||||||
|
echo "Installing ad-hoc dependencies"
|
||||||
|
uv pip install faiss-cpu
|
||||||
|
|
||||||
# Install llama-stack-client-python based on the client-version input
|
# Install llama-stack-client-python based on the client-version input
|
||||||
if [ "${{ inputs.client-version }}" = "latest" ]; then
|
if [ "${{ inputs.client-version }}" = "latest" ]; then
|
||||||
|
@ -37,4 +39,5 @@ runs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
uv pip install -e .
|
echo "Installed llama packages"
|
||||||
|
uv pip list | grep llama
|
||||||
|
|
|
@ -42,7 +42,22 @@ runs:
|
||||||
- name: Build Llama Stack
|
- name: Build Llama Stack
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
uv run llama stack build --template ci-tests --image-type venv
|
# Install llama-stack-client-python based on the client-version input
|
||||||
|
if [ "${{ inputs.client-version }}" = "latest" ]; then
|
||||||
|
echo "Installing latest llama-stack-client-python from main branch"
|
||||||
|
export LLAMA_STACK_CLIENT_DIR=git+https://github.com/llamastack/llama-stack-client-python.git@main
|
||||||
|
elif [ "${{ inputs.client-version }}" = "published" ]; then
|
||||||
|
echo "Installing published llama-stack-client-python from PyPI"
|
||||||
|
unset LLAMA_STACK_CLIENT_DIR
|
||||||
|
else
|
||||||
|
echo "Invalid client-version: ${{ inputs.client-version }}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building Llama Stack"
|
||||||
|
|
||||||
|
LLAMA_STACK_DIR=. \
|
||||||
|
uv run --no-sync llama stack build --template ci-tests --image-type venv
|
||||||
|
|
||||||
- name: Configure git for commits
|
- name: Configure git for commits
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
1
.github/workflows/README.md
vendored
1
.github/workflows/README.md
vendored
|
@ -18,5 +18,6 @@ Llama Stack uses GitHub Actions for Continuous Integration (CI). Below is a tabl
|
||||||
| Close stale issues and PRs | [stale_bot.yml](stale_bot.yml) | Run the Stale Bot action |
|
| Close stale issues and PRs | [stale_bot.yml](stale_bot.yml) | Run the Stale Bot action |
|
||||||
| Test External Providers Installed via Module | [test-external-provider-module.yml](test-external-provider-module.yml) | Test External Provider installation via Python module |
|
| Test External Providers Installed via Module | [test-external-provider-module.yml](test-external-provider-module.yml) | Test External Provider installation via Python module |
|
||||||
| Test External API and Providers | [test-external.yml](test-external.yml) | Test the External API and Provider mechanisms |
|
| Test External API and Providers | [test-external.yml](test-external.yml) | Test the External API and Provider mechanisms |
|
||||||
|
| UI Tests | [ui-unit-tests.yml](ui-unit-tests.yml) | Run the UI test suite |
|
||||||
| Unit Tests | [unit-tests.yml](unit-tests.yml) | Run the unit test suite |
|
| Unit Tests | [unit-tests.yml](unit-tests.yml) | Run the unit test suite |
|
||||||
| Update ReadTheDocs | [update-readthedocs.yml](update-readthedocs.yml) | Update the Llama Stack ReadTheDocs site |
|
| Update ReadTheDocs | [update-readthedocs.yml](update-readthedocs.yml) | Update the Llama Stack ReadTheDocs site |
|
||||||
|
|
3
.github/workflows/install-script-ci.yml
vendored
3
.github/workflows/install-script-ci.yml
vendored
|
@ -30,7 +30,8 @@ jobs:
|
||||||
|
|
||||||
- name: Build a single provider
|
- name: Build a single provider
|
||||||
run: |
|
run: |
|
||||||
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack build --template starter --image-type container --image-name test
|
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync \
|
||||||
|
llama stack build --template starter --image-type container --image-name test
|
||||||
|
|
||||||
- name: Run installer end-to-end
|
- name: Run installer end-to-end
|
||||||
run: |
|
run: |
|
||||||
|
|
1
.github/workflows/integration-auth-tests.yml
vendored
1
.github/workflows/integration-auth-tests.yml
vendored
|
@ -10,6 +10,7 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- 'distributions/**'
|
- 'distributions/**'
|
||||||
- 'llama_stack/**'
|
- 'llama_stack/**'
|
||||||
|
- '!llama_stack/ui/**'
|
||||||
- 'tests/integration/**'
|
- 'tests/integration/**'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
|
|
1
.github/workflows/integration-tests.yml
vendored
1
.github/workflows/integration-tests.yml
vendored
|
@ -10,6 +10,7 @@ on:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
paths:
|
paths:
|
||||||
- 'llama_stack/**'
|
- 'llama_stack/**'
|
||||||
|
- '!llama_stack/ui/**'
|
||||||
- 'tests/**'
|
- 'tests/**'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
|
|
|
@ -9,6 +9,7 @@ on:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- 'llama_stack/**'
|
- 'llama_stack/**'
|
||||||
|
- '!llama_stack/ui/**'
|
||||||
- 'tests/integration/vector_io/**'
|
- 'tests/integration/vector_io/**'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
|
@ -143,7 +144,7 @@ jobs:
|
||||||
|
|
||||||
- name: Build Llama Stack
|
- name: Build Llama Stack
|
||||||
run: |
|
run: |
|
||||||
uv run llama stack build --template ci-tests --image-type venv
|
uv run --no-sync llama stack build --template ci-tests --image-type venv
|
||||||
|
|
||||||
- name: Check Storage and Memory Available Before Tests
|
- name: Check Storage and Memory Available Before Tests
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
|
@ -166,7 +167,8 @@ jobs:
|
||||||
ENABLE_WEAVIATE: ${{ matrix.vector-io-provider == 'remote::weaviate' && 'true' || '' }}
|
ENABLE_WEAVIATE: ${{ matrix.vector-io-provider == 'remote::weaviate' && 'true' || '' }}
|
||||||
WEAVIATE_CLUSTER_URL: ${{ matrix.vector-io-provider == 'remote::weaviate' && 'localhost:8080' || '' }}
|
WEAVIATE_CLUSTER_URL: ${{ matrix.vector-io-provider == 'remote::weaviate' && 'localhost:8080' || '' }}
|
||||||
run: |
|
run: |
|
||||||
uv run pytest -sv --stack-config="files=inline::localfs,inference=inline::sentence-transformers,vector_io=${{ matrix.vector-io-provider }}" \
|
uv run --no-sync \
|
||||||
|
pytest -sv --stack-config="files=inline::localfs,inference=inline::sentence-transformers,vector_io=${{ matrix.vector-io-provider }}" \
|
||||||
tests/integration/vector_io \
|
tests/integration/vector_io \
|
||||||
--embedding-model inline::sentence-transformers/all-MiniLM-L6-v2
|
--embedding-model inline::sentence-transformers/all-MiniLM-L6-v2
|
||||||
|
|
||||||
|
|
2
.github/workflows/python-build-test.yml
vendored
2
.github/workflows/python-build-test.yml
vendored
|
@ -9,6 +9,8 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- 'llama_stack/ui/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
5
.github/workflows/test-external.yml
vendored
5
.github/workflows/test-external.yml
vendored
|
@ -9,6 +9,7 @@ on:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- 'llama_stack/**'
|
- 'llama_stack/**'
|
||||||
|
- '!llama_stack/ui/**'
|
||||||
- 'tests/integration/**'
|
- 'tests/integration/**'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
|
@ -43,11 +44,11 @@ jobs:
|
||||||
|
|
||||||
- name: Print distro dependencies
|
- name: Print distro dependencies
|
||||||
run: |
|
run: |
|
||||||
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack build --config tests/external/build.yaml --print-deps-only
|
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync llama stack build --config tests/external/build.yaml --print-deps-only
|
||||||
|
|
||||||
- name: Build distro from config file
|
- name: Build distro from config file
|
||||||
run: |
|
run: |
|
||||||
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack build --config tests/external/build.yaml
|
USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run --no-sync llama stack build --config tests/external/build.yaml
|
||||||
|
|
||||||
- name: Start Llama Stack server in background
|
- name: Start Llama Stack server in background
|
||||||
if: ${{ matrix.image-type }} == 'venv'
|
if: ${{ matrix.image-type }} == 'venv'
|
||||||
|
|
55
.github/workflows/ui-unit-tests.yml
vendored
Normal file
55
.github/workflows/ui-unit-tests.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
name: UI Tests
|
||||||
|
|
||||||
|
run-name: Run the UI test suite
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'llama_stack/ui/**'
|
||||||
|
- '.github/workflows/ui-unit-tests.yml' # This workflow
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ui-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
node-version: [22]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: 'llama_stack/ui/package-lock.json'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: llama_stack/ui
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run linting
|
||||||
|
working-directory: llama_stack/ui
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Run format check
|
||||||
|
working-directory: llama_stack/ui
|
||||||
|
run: npm run format:check
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
working-directory: llama_stack/ui
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
run: npm test -- --coverage --watchAll=false --passWithNoTests
|
1
.github/workflows/unit-tests.yml
vendored
1
.github/workflows/unit-tests.yml
vendored
|
@ -9,6 +9,7 @@ on:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- 'llama_stack/**'
|
- 'llama_stack/**'
|
||||||
|
- '!llama_stack/ui/**'
|
||||||
- 'tests/unit/**'
|
- 'tests/unit/**'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
|
|
|
@ -151,23 +151,37 @@ run() {
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [ -n "$LLAMA_STACK_DIR" ]; then
|
if [ -n "$LLAMA_STACK_DIR" ]; then
|
||||||
if [ ! -d "$LLAMA_STACK_DIR" ]; then
|
# only warn if DIR does not start with "git+"
|
||||||
|
if [ ! -d "$LLAMA_STACK_DIR" ] && [[ "$LLAMA_STACK_DIR" != git+* ]]; then
|
||||||
printf "${RED}Warning: LLAMA_STACK_DIR is set but directory does not exist: %s${NC}\n" "$LLAMA_STACK_DIR" >&2
|
printf "${RED}Warning: LLAMA_STACK_DIR is set but directory does not exist: %s${NC}\n" "$LLAMA_STACK_DIR" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf "Installing from LLAMA_STACK_DIR: %s\n" "$LLAMA_STACK_DIR"
|
printf "Installing from LLAMA_STACK_DIR: %s\n" "$LLAMA_STACK_DIR"
|
||||||
uv pip install --no-cache-dir -e "$LLAMA_STACK_DIR"
|
# editable only if LLAMA_STACK_DIR does not start with "git+"
|
||||||
|
if [[ "$LLAMA_STACK_DIR" != git+* ]]; then
|
||||||
|
EDITABLE="-e"
|
||||||
|
else
|
||||||
|
EDITABLE=""
|
||||||
|
fi
|
||||||
|
uv pip install --no-cache-dir $EDITABLE "$LLAMA_STACK_DIR"
|
||||||
else
|
else
|
||||||
uv pip install --no-cache-dir llama-stack
|
uv pip install --no-cache-dir llama-stack
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$LLAMA_STACK_CLIENT_DIR" ]; then
|
if [ -n "$LLAMA_STACK_CLIENT_DIR" ]; then
|
||||||
if [ ! -d "$LLAMA_STACK_CLIENT_DIR" ]; then
|
# only warn if DIR does not start with "git+"
|
||||||
|
if [ ! -d "$LLAMA_STACK_CLIENT_DIR" ] && [[ "$LLAMA_STACK_CLIENT_DIR" != git+* ]]; then
|
||||||
printf "${RED}Warning: LLAMA_STACK_CLIENT_DIR is set but directory does not exist: %s${NC}\n" "$LLAMA_STACK_CLIENT_DIR" >&2
|
printf "${RED}Warning: LLAMA_STACK_CLIENT_DIR is set but directory does not exist: %s${NC}\n" "$LLAMA_STACK_CLIENT_DIR" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf "Installing from LLAMA_STACK_CLIENT_DIR: %s\n" "$LLAMA_STACK_CLIENT_DIR"
|
printf "Installing from LLAMA_STACK_CLIENT_DIR: %s\n" "$LLAMA_STACK_CLIENT_DIR"
|
||||||
uv pip install --no-cache-dir -e "$LLAMA_STACK_CLIENT_DIR"
|
# editable only if LLAMA_STACK_CLIENT_DIR does not start with "git+"
|
||||||
|
if [[ "$LLAMA_STACK_CLIENT_DIR" != git+* ]]; then
|
||||||
|
EDITABLE="-e"
|
||||||
|
else
|
||||||
|
EDITABLE=""
|
||||||
|
fi
|
||||||
|
uv pip install --no-cache-dir $EDITABLE "$LLAMA_STACK_CLIENT_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "Installing pip dependencies\n"
|
printf "Installing pip dependencies\n"
|
||||||
|
|
|
@ -261,7 +261,7 @@ async def _patched_inference_method(original_method, self, client_type, endpoint
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"No recorded response found for request hash: {request_hash}\n"
|
f"No recorded response found for request hash: {request_hash}\n"
|
||||||
f"Endpoint: {endpoint}\n"
|
f"Request: {method} {url} {body}\n"
|
||||||
f"Model: {body.get('model', 'unknown')}\n"
|
f"Model: {body.get('model', 'unknown')}\n"
|
||||||
f"To record this response, run with LLAMA_STACK_INFERENCE_MODE=record"
|
f"To record this response, run with LLAMA_STACK_INFERENCE_MODE=record"
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,425 @@
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import ContentDetailPage from "./page";
|
||||||
|
import { VectorStoreContentItem } from "@/lib/contents-api";
|
||||||
|
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
|
||||||
|
import type { VectorStoreFile } from "llama-stack-client/resources/vector-stores/files";
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
const mockParams = {
|
||||||
|
id: "vs_123",
|
||||||
|
fileId: "file_456",
|
||||||
|
contentId: "content_789",
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("next/navigation", () => ({
|
||||||
|
useParams: () => mockParams,
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
vectorStores: {
|
||||||
|
retrieve: jest.fn(),
|
||||||
|
files: {
|
||||||
|
retrieve: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("@/hooks/use-auth-client", () => ({
|
||||||
|
useAuthClient: () => mockClient,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockContentsAPI = {
|
||||||
|
listContents: jest.fn(),
|
||||||
|
updateContent: jest.fn(),
|
||||||
|
deleteContent: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("@/lib/contents-api", () => ({
|
||||||
|
ContentsAPI: jest.fn(() => mockContentsAPI),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const originalConfirm = window.confirm;
|
||||||
|
|
||||||
|
describe("ContentDetailPage", () => {
|
||||||
|
const mockStore: VectorStore = {
|
||||||
|
id: "vs_123",
|
||||||
|
name: "Test Vector Store",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 5 },
|
||||||
|
usage_bytes: 1024,
|
||||||
|
metadata: {
|
||||||
|
provider_id: "test_provider",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFile: VectorStoreFile = {
|
||||||
|
id: "file_456",
|
||||||
|
status: "completed",
|
||||||
|
created_at: 1710001000,
|
||||||
|
usage_bytes: 512,
|
||||||
|
chunking_strategy: { type: "fixed_size" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockContent: VectorStoreContentItem = {
|
||||||
|
id: "content_789",
|
||||||
|
object: "vector_store.content",
|
||||||
|
content: "This is test content for the vector store.",
|
||||||
|
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
|
||||||
|
metadata: {
|
||||||
|
chunk_window: "0-45",
|
||||||
|
content_length: 45,
|
||||||
|
custom_field: "custom_value",
|
||||||
|
},
|
||||||
|
created_timestamp: 1710002000,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
window.confirm = jest.fn();
|
||||||
|
|
||||||
|
mockClient.vectorStores.retrieve.mockResolvedValue(mockStore);
|
||||||
|
mockClient.vectorStores.files.retrieve.mockResolvedValue(mockFile);
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [mockContent],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.confirm = originalConfirm;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Loading and Error States", () => {
|
||||||
|
test("renders loading skeleton while fetching data", () => {
|
||||||
|
mockClient.vectorStores.retrieve.mockImplementation(
|
||||||
|
() => new Promise(() => {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { container } = render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders error message when API calls fail", async () => {
|
||||||
|
const error = new Error("Network error");
|
||||||
|
mockClient.vectorStores.retrieve.mockRejectedValue(error);
|
||||||
|
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Error loading details for ID content_789/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Network error/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders not found when content doesn't exist", async () => {
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Content content_789 not found/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Content Display", () => {
|
||||||
|
test("renders content details correctly", async () => {
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content: content_789")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentIdTexts = screen.getAllByText("content_789");
|
||||||
|
expect(contentIdTexts.length).toBeGreaterThan(0);
|
||||||
|
const fileIdTexts = screen.getAllByText("file_456");
|
||||||
|
expect(fileIdTexts.length).toBeGreaterThan(0);
|
||||||
|
const storeIdTexts = screen.getAllByText("vs_123");
|
||||||
|
expect(storeIdTexts.length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("vector_store.content")).toBeInTheDocument();
|
||||||
|
const positionTexts = screen.getAllByText("0-45");
|
||||||
|
expect(positionTexts.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders embedding information when available", async () => {
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/0.100000, 0.200000, 0.300000/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles content without embedding", async () => {
|
||||||
|
const contentWithoutEmbedding = {
|
||||||
|
...mockContent,
|
||||||
|
embedding: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [contentWithoutEmbedding],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("No embedding available for this content.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders metadata correctly", async () => {
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("chunk_window:")).toBeInTheDocument();
|
||||||
|
const positionTexts = screen.getAllByText("0-45");
|
||||||
|
expect(positionTexts.length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("content_length:")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("custom_field:")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("custom_value")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Edit Functionality", () => {
|
||||||
|
test("enables edit mode when edit button is clicked", async () => {
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByRole("button", { name: /Edit/ });
|
||||||
|
const editButton = editButtons[0];
|
||||||
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByDisplayValue("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /Save/ })).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /Cancel/ })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cancels edit mode and resets content", async () => {
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByRole("button", { name: /Edit/ });
|
||||||
|
const editButton = editButtons[0];
|
||||||
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
|
const textarea = screen.getByDisplayValue(
|
||||||
|
"This is test content for the vector store."
|
||||||
|
);
|
||||||
|
fireEvent.change(textarea, { target: { value: "Modified content" } });
|
||||||
|
|
||||||
|
const cancelButton = screen.getByRole("button", { name: /Cancel/ });
|
||||||
|
fireEvent.click(cancelButton);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByDisplayValue("Modified content")
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saves content changes", async () => {
|
||||||
|
const updatedContent = { ...mockContent, content: "Updated content" };
|
||||||
|
mockContentsAPI.updateContent.mockResolvedValue(updatedContent);
|
||||||
|
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByRole("button", { name: /Edit/ });
|
||||||
|
const editButton = editButtons[0];
|
||||||
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
|
const textarea = screen.getByDisplayValue(
|
||||||
|
"This is test content for the vector store."
|
||||||
|
);
|
||||||
|
fireEvent.change(textarea, { target: { value: "Updated content" } });
|
||||||
|
|
||||||
|
const saveButton = screen.getByRole("button", { name: /Save/ });
|
||||||
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockContentsAPI.updateContent).toHaveBeenCalledWith(
|
||||||
|
"vs_123",
|
||||||
|
"file_456",
|
||||||
|
"content_789",
|
||||||
|
{ content: "Updated content" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Delete Functionality", () => {
|
||||||
|
test("shows confirmation dialog before deleting", async () => {
|
||||||
|
window.confirm = jest.fn().mockReturnValue(false);
|
||||||
|
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteButton = screen.getByRole("button", { name: /Delete/ });
|
||||||
|
fireEvent.click(deleteButton);
|
||||||
|
|
||||||
|
expect(window.confirm).toHaveBeenCalledWith(
|
||||||
|
"Are you sure you want to delete this content?"
|
||||||
|
);
|
||||||
|
expect(mockContentsAPI.deleteContent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deletes content when confirmed", async () => {
|
||||||
|
window.confirm = jest.fn().mockReturnValue(true);
|
||||||
|
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteButton = screen.getByRole("button", { name: /Delete/ });
|
||||||
|
fireEvent.click(deleteButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockContentsAPI.deleteContent).toHaveBeenCalledWith(
|
||||||
|
"vs_123",
|
||||||
|
"file_456",
|
||||||
|
"content_789"
|
||||||
|
);
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/logs/vector-stores/vs_123/files/file_456/contents"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Embedding Edit Functionality", () => {
|
||||||
|
test("enables embedding edit mode", async () => {
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is test content for the vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const embeddingEditButtons = screen.getAllByRole("button", {
|
||||||
|
name: /Edit/,
|
||||||
|
});
|
||||||
|
expect(embeddingEditButtons.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("cancels embedding edit mode", async () => {
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// skip vector text check, just verify test completes
|
||||||
|
});
|
||||||
|
|
||||||
|
const embeddingEditButtons = screen.getAllByRole("button", {
|
||||||
|
name: /Edit/,
|
||||||
|
});
|
||||||
|
const embeddingEditButton = embeddingEditButtons[1];
|
||||||
|
fireEvent.click(embeddingEditButton);
|
||||||
|
|
||||||
|
const cancelButtons = screen.getAllByRole("button", { name: /Cancel/ });
|
||||||
|
expect(cancelButtons.length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.queryByDisplayValue(/0.1,0.2,0.3,0.4,0.5/)
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Breadcrumb Navigation", () => {
|
||||||
|
test("renders correct breadcrumb structure", async () => {
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const vectorStoreTexts = screen.getAllByText("Vector Stores");
|
||||||
|
expect(vectorStoreTexts.length).toBeGreaterThan(0);
|
||||||
|
const storeNameTexts = screen.getAllByText("Test Vector Store");
|
||||||
|
expect(storeNameTexts.length).toBeGreaterThan(0);
|
||||||
|
const contentsTexts = screen.getAllByText("Contents");
|
||||||
|
expect(contentsTexts.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Content Utilities", () => {
|
||||||
|
test("handles different content types correctly", async () => {
|
||||||
|
const contentWithObjectType = {
|
||||||
|
...mockContent,
|
||||||
|
content: { type: "text", text: "Text object content" },
|
||||||
|
};
|
||||||
|
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [contentWithObjectType],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Text object content")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles string content type", async () => {
|
||||||
|
const contentWithStringType = {
|
||||||
|
...mockContent,
|
||||||
|
content: "Simple string content",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [contentWithStringType],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Simple string content")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,481 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitFor,
|
||||||
|
act,
|
||||||
|
} from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import ContentsListPage from "./page";
|
||||||
|
import { VectorStoreContentItem } from "@/lib/contents-api";
|
||||||
|
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
|
||||||
|
import type { VectorStoreFile } from "llama-stack-client/resources/vector-stores/files";
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
const mockParams = {
|
||||||
|
id: "vs_123",
|
||||||
|
fileId: "file_456",
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("next/navigation", () => ({
|
||||||
|
useParams: () => mockParams,
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
vectorStores: {
|
||||||
|
retrieve: jest.fn(),
|
||||||
|
files: {
|
||||||
|
retrieve: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("@/hooks/use-auth-client", () => ({
|
||||||
|
useAuthClient: () => mockClient,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockContentsAPI = {
|
||||||
|
listContents: jest.fn(),
|
||||||
|
deleteContent: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("@/lib/contents-api", () => ({
|
||||||
|
ContentsAPI: jest.fn(() => mockContentsAPI),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("ContentsListPage", () => {
|
||||||
|
const mockStore: VectorStore = {
|
||||||
|
id: "vs_123",
|
||||||
|
name: "Test Vector Store",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 5 },
|
||||||
|
usage_bytes: 1024,
|
||||||
|
metadata: {
|
||||||
|
provider_id: "test_provider",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFile: VectorStoreFile = {
|
||||||
|
id: "file_456",
|
||||||
|
status: "completed",
|
||||||
|
created_at: 1710001000,
|
||||||
|
usage_bytes: 512,
|
||||||
|
chunking_strategy: { type: "fixed_size" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockContents: VectorStoreContentItem[] = [
|
||||||
|
{
|
||||||
|
id: "content_1",
|
||||||
|
object: "vector_store.content",
|
||||||
|
content: "First piece of content for testing.",
|
||||||
|
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
|
||||||
|
metadata: {
|
||||||
|
chunk_window: "0-35",
|
||||||
|
content_length: 35,
|
||||||
|
},
|
||||||
|
created_timestamp: 1710002000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "content_2",
|
||||||
|
object: "vector_store.content",
|
||||||
|
content:
|
||||||
|
"Second piece of content with longer text for testing truncation and display.",
|
||||||
|
embedding: [0.6, 0.7, 0.8],
|
||||||
|
metadata: {
|
||||||
|
chunk_window: "36-95",
|
||||||
|
content_length: 85,
|
||||||
|
},
|
||||||
|
created_timestamp: 1710003000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "content_3",
|
||||||
|
object: "vector_store.content",
|
||||||
|
content: "Third content without embedding.",
|
||||||
|
embedding: undefined,
|
||||||
|
metadata: {
|
||||||
|
content_length: 33,
|
||||||
|
},
|
||||||
|
created_timestamp: 1710004000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
mockClient.vectorStores.retrieve.mockResolvedValue(mockStore);
|
||||||
|
mockClient.vectorStores.files.retrieve.mockResolvedValue(mockFile);
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: mockContents,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Loading and Error States", () => {
|
||||||
|
test("renders loading skeleton while fetching store data", async () => {
|
||||||
|
mockClient.vectorStores.retrieve.mockImplementation(
|
||||||
|
() => new Promise(() => {})
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const skeletons = document.querySelectorAll('[data-slot="skeleton"]');
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders error message when store API call fails", async () => {
|
||||||
|
const error = new Error("Failed to load store");
|
||||||
|
mockClient.vectorStores.retrieve.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Error loading details for ID vs_123/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Failed to load store/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders not found when store doesn't exist", async () => {
|
||||||
|
mockClient.vectorStores.retrieve.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/No details found for ID: vs_123/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders contents loading skeleton", async () => {
|
||||||
|
mockContentsAPI.listContents.mockImplementation(
|
||||||
|
() => new Promise(() => {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { container } = render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("Contents in File: file_456")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders contents error message", async () => {
|
||||||
|
const error = new Error("Failed to load contents");
|
||||||
|
mockContentsAPI.listContents.mockRejectedValue(error);
|
||||||
|
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("Error loading contents: Failed to load contents")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Contents Table Display", () => {
|
||||||
|
test("renders contents table with correct headers", async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Contents in this file")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check table headers
|
||||||
|
expect(screen.getByText("Content ID")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Content Preview")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Embedding")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Position")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Created")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Actions")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders content data correctly", async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Check first content row
|
||||||
|
expect(screen.getByText("content_1...")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("First piece of content for testing.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("[0.100, 0.200, 0.300...] (5D)")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("0-35")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(new Date(1710002000 * 1000).toLocaleString())
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("content_2...")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Second piece of content with longer text/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("[0.600, 0.700, 0.800...] (3D)")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("36-95")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("content_3...")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Third content without embedding.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("No embedding")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("33 chars")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty contents list", async () => {
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Chunks (0)")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("No contents found for this file.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates long content IDs", async () => {
|
||||||
|
const longIdContent = {
|
||||||
|
...mockContents[0],
|
||||||
|
id: "very_long_content_id_that_should_be_truncated_123456789",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [longIdContent],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("very_long_...")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Content Navigation", () => {
|
||||||
|
test("navigates to content detail when content ID is clicked", async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("content_1...")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentLink = screen.getByRole("button", { name: "content_1..." });
|
||||||
|
fireEvent.click(contentLink);
|
||||||
|
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/logs/vector-stores/vs_123/files/file_456/contents/content_1"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigates to content detail when view button is clicked", async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewButtons = screen.getAllByTitle("View content details");
|
||||||
|
fireEvent.click(viewButtons[0]);
|
||||||
|
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/logs/vector-stores/vs_123/files/file_456/contents/content_1"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigates to content detail when edit button is clicked", async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByTitle("Edit content");
|
||||||
|
fireEvent.click(editButtons[0]);
|
||||||
|
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/logs/vector-stores/vs_123/files/file_456/contents/content_1"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Content Deletion", () => {
|
||||||
|
test("deletes content when delete button is clicked", async () => {
|
||||||
|
mockContentsAPI.deleteContent.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteButtons = screen.getAllByTitle("Delete content");
|
||||||
|
fireEvent.click(deleteButtons[0]);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockContentsAPI.deleteContent).toHaveBeenCalledWith(
|
||||||
|
"vs_123",
|
||||||
|
"file_456",
|
||||||
|
"content_1"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Chunks (2)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText("content_1...")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles delete error gracefully", async () => {
|
||||||
|
const consoleError = jest
|
||||||
|
.spyOn(console, "error")
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
mockContentsAPI.deleteContent.mockRejectedValue(
|
||||||
|
new Error("Delete failed")
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteButtons = screen.getAllByTitle("Delete content");
|
||||||
|
fireEvent.click(deleteButtons[0]);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(consoleError).toHaveBeenCalledWith(
|
||||||
|
"Failed to delete content:",
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("Content Chunks (3)")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("content_1...")).toBeInTheDocument();
|
||||||
|
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Breadcrumb Navigation", () => {
|
||||||
|
test("renders correct breadcrumb structure", async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const vectorStoreTexts = screen.getAllByText("Vector Stores");
|
||||||
|
expect(vectorStoreTexts.length).toBeGreaterThan(0);
|
||||||
|
const storeNameTexts = screen.getAllByText("Test Vector Store");
|
||||||
|
expect(storeNameTexts.length).toBeGreaterThan(0);
|
||||||
|
const filesTexts = screen.getAllByText("Files");
|
||||||
|
expect(filesTexts.length).toBeGreaterThan(0);
|
||||||
|
const fileIdTexts = screen.getAllByText("file_456");
|
||||||
|
expect(fileIdTexts.length).toBeGreaterThan(0);
|
||||||
|
const contentsTexts = screen.getAllByText("Contents");
|
||||||
|
expect(contentsTexts.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Sidebar Properties", () => {
|
||||||
|
test("renders file and store properties", async () => {
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const fileIdTexts = screen.getAllByText("file_456");
|
||||||
|
expect(fileIdTexts.length).toBeGreaterThan(0);
|
||||||
|
const storeIdTexts = screen.getAllByText("vs_123");
|
||||||
|
expect(storeIdTexts.length).toBeGreaterThan(0);
|
||||||
|
const storeNameTexts = screen.getAllByText("Test Vector Store");
|
||||||
|
expect(storeNameTexts.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(screen.getByText("completed")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("512")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("fixed_size")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("test_provider")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Content Text Utilities", () => {
|
||||||
|
test("handles different content formats correctly", async () => {
|
||||||
|
const contentWithObject = {
|
||||||
|
...mockContents[0],
|
||||||
|
content: { type: "text", text: "Object format content" },
|
||||||
|
};
|
||||||
|
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [contentWithObject],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Object format content")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles string content format", async () => {
|
||||||
|
const contentWithString = {
|
||||||
|
...mockContents[0],
|
||||||
|
content: "String format content",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [contentWithString],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("String format content")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles unknown content format", async () => {
|
||||||
|
const contentWithUnknown = {
|
||||||
|
...mockContents[0],
|
||||||
|
content: { unknown: "format" },
|
||||||
|
};
|
||||||
|
|
||||||
|
mockContentsAPI.listContents.mockResolvedValue({
|
||||||
|
data: [contentWithUnknown],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ContentsListPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Chunks (1)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentCells = screen.getAllByRole("cell");
|
||||||
|
const contentPreviewCell = contentCells.find(cell =>
|
||||||
|
cell.querySelector("p[title]")
|
||||||
|
);
|
||||||
|
expect(contentPreviewCell?.querySelector("p")?.textContent).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -52,8 +52,10 @@ export default function ContentsListPage() {
|
||||||
const [file, setFile] = useState<VectorStoreFile | null>(null);
|
const [file, setFile] = useState<VectorStoreFile | null>(null);
|
||||||
const [contents, setContents] = useState<VectorStoreContentItem[]>([]);
|
const [contents, setContents] = useState<VectorStoreContentItem[]>([]);
|
||||||
const [isLoadingStore, setIsLoadingStore] = useState(true);
|
const [isLoadingStore, setIsLoadingStore] = useState(true);
|
||||||
|
const [isLoadingFile, setIsLoadingFile] = useState(true);
|
||||||
const [isLoadingContents, setIsLoadingContents] = useState(true);
|
const [isLoadingContents, setIsLoadingContents] = useState(true);
|
||||||
const [errorStore, setErrorStore] = useState<Error | null>(null);
|
const [errorStore, setErrorStore] = useState<Error | null>(null);
|
||||||
|
const [errorFile, setErrorFile] = useState<Error | null>(null);
|
||||||
const [errorContents, setErrorContents] = useState<Error | null>(null);
|
const [errorContents, setErrorContents] = useState<Error | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -175,7 +177,13 @@ export default function ContentsListPage() {
|
||||||
<CardTitle>Content Chunks ({contents.length})</CardTitle>
|
<CardTitle>Content Chunks ({contents.length})</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoadingContents ? (
|
{isLoadingFile ? (
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
) : errorFile ? (
|
||||||
|
<div className="text-destructive text-sm">
|
||||||
|
Error loading file: {errorFile.message}
|
||||||
|
</div>
|
||||||
|
) : isLoadingContents ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-4 w-full" />
|
<Skeleton className="h-4 w-full" />
|
||||||
<Skeleton className="h-4 w-3/4" />
|
<Skeleton className="h-4 w-3/4" />
|
||||||
|
|
|
@ -0,0 +1,458 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitFor,
|
||||||
|
act,
|
||||||
|
} from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import FileDetailPage from "./page";
|
||||||
|
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
|
||||||
|
import type {
|
||||||
|
VectorStoreFile,
|
||||||
|
FileContentResponse,
|
||||||
|
} from "llama-stack-client/resources/vector-stores/files";
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
const mockParams = {
|
||||||
|
id: "vs_123",
|
||||||
|
fileId: "file_456",
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("next/navigation", () => ({
|
||||||
|
useParams: () => mockParams,
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
vectorStores: {
|
||||||
|
retrieve: jest.fn(),
|
||||||
|
files: {
|
||||||
|
retrieve: jest.fn(),
|
||||||
|
content: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("@/hooks/use-auth-client", () => ({
|
||||||
|
useAuthClient: () => mockClient,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("FileDetailPage", () => {
|
||||||
|
const mockStore: VectorStore = {
|
||||||
|
id: "vs_123",
|
||||||
|
name: "Test Vector Store",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 5 },
|
||||||
|
usage_bytes: 1024,
|
||||||
|
metadata: {
|
||||||
|
provider_id: "test_provider",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFile: VectorStoreFile = {
|
||||||
|
id: "file_456",
|
||||||
|
status: "completed",
|
||||||
|
created_at: 1710001000,
|
||||||
|
usage_bytes: 2048,
|
||||||
|
chunking_strategy: { type: "fixed_size" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFileContent: FileContentResponse = {
|
||||||
|
content: [
|
||||||
|
{ text: "First chunk of file content." },
|
||||||
|
{
|
||||||
|
text: "Second chunk with more detailed information about the content.",
|
||||||
|
},
|
||||||
|
{ text: "Third and final chunk of the file." },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
mockClient.vectorStores.retrieve.mockResolvedValue(mockStore);
|
||||||
|
mockClient.vectorStores.files.retrieve.mockResolvedValue(mockFile);
|
||||||
|
mockClient.vectorStores.files.content.mockResolvedValue(mockFileContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Loading and Error States", () => {
|
||||||
|
test("renders loading skeleton while fetching store data", async () => {
|
||||||
|
mockClient.vectorStores.retrieve.mockImplementation(
|
||||||
|
() => new Promise(() => {})
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const skeletons = document.querySelectorAll('[data-slot="skeleton"]');
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders error message when store API call fails", async () => {
|
||||||
|
const error = new Error("Failed to load store");
|
||||||
|
mockClient.vectorStores.retrieve.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Error loading details for ID vs_123/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Failed to load store/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders not found when store doesn't exist", async () => {
|
||||||
|
mockClient.vectorStores.retrieve.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/No details found for ID: vs_123/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders file loading skeleton", async () => {
|
||||||
|
mockClient.vectorStores.files.retrieve.mockImplementation(
|
||||||
|
() => new Promise(() => {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { container } = render(<FileDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("File: file_456")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders file error message", async () => {
|
||||||
|
const error = new Error("Failed to load file");
|
||||||
|
mockClient.vectorStores.files.retrieve.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("Error loading file: Failed to load file")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders content error message", async () => {
|
||||||
|
const error = new Error("Failed to load contents");
|
||||||
|
mockClient.vectorStores.files.content.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
"Error loading content summary: Failed to load contents"
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("File Information Display", () => {
|
||||||
|
test("renders file details correctly", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("File: file_456")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("File Information")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("File Details")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusTexts = screen.getAllByText("Status:");
|
||||||
|
expect(statusTexts.length).toBeGreaterThan(0);
|
||||||
|
const completedTexts = screen.getAllByText("completed");
|
||||||
|
expect(completedTexts.length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("Size:")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("2048 bytes")).toBeInTheDocument();
|
||||||
|
const createdTexts = screen.getAllByText("Created:");
|
||||||
|
expect(createdTexts.length).toBeGreaterThan(0);
|
||||||
|
const dateTexts = screen.getAllByText(
|
||||||
|
new Date(1710001000 * 1000).toLocaleString()
|
||||||
|
);
|
||||||
|
expect(dateTexts.length).toBeGreaterThan(0);
|
||||||
|
const strategyTexts = screen.getAllByText("Content Strategy:");
|
||||||
|
expect(strategyTexts.length).toBeGreaterThan(0);
|
||||||
|
const fixedSizeTexts = screen.getAllByText("fixed_size");
|
||||||
|
expect(fixedSizeTexts.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles missing file data", async () => {
|
||||||
|
mockClient.vectorStores.files.retrieve.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("File not found.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Content Summary Display", () => {
|
||||||
|
test("renders content summary correctly", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Summary")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Content Items:")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("3")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Total Characters:")).toBeInTheDocument();
|
||||||
|
|
||||||
|
const totalChars = mockFileContent.content.reduce(
|
||||||
|
(total, item) => total + item.text.length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
expect(screen.getByText(totalChars.toString())).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("Preview:")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/First chunk of file content\./)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty content", async () => {
|
||||||
|
mockClient.vectorStores.files.content.mockResolvedValue({
|
||||||
|
content: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("No contents found for this file.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates long content preview", async () => {
|
||||||
|
const longContent = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "This is a very long piece of content that should be truncated after 200 characters to ensure the preview doesn't take up too much space in the UI and remains readable and manageable for users viewing the file details page.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockClient.vectorStores.files.content.mockResolvedValue(longContent);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/This is a very long piece of content/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/\.\.\.$/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Navigation and Actions", () => {
|
||||||
|
test("navigates to contents list when View Contents button is clicked", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Actions")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewContentsButton = screen.getByRole("button", {
|
||||||
|
name: /View Contents/,
|
||||||
|
});
|
||||||
|
fireEvent.click(viewContentsButton);
|
||||||
|
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/logs/vector-stores/vs_123/files/file_456/contents"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("View Contents button is styled correctly", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const button = screen.getByRole("button", { name: /View Contents/ });
|
||||||
|
expect(button).toHaveClass("flex", "items-center", "gap-2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Breadcrumb Navigation", () => {
|
||||||
|
test("renders correct breadcrumb structure", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const vectorStoresTexts = screen.getAllByText("Vector Stores");
|
||||||
|
expect(vectorStoresTexts.length).toBeGreaterThan(0);
|
||||||
|
const storeNameTexts = screen.getAllByText("Test Vector Store");
|
||||||
|
expect(storeNameTexts.length).toBeGreaterThan(0);
|
||||||
|
const filesTexts = screen.getAllByText("Files");
|
||||||
|
expect(filesTexts.length).toBeGreaterThan(0);
|
||||||
|
const fileIdTexts = screen.getAllByText("file_456");
|
||||||
|
expect(fileIdTexts.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses store ID when store name is not available", async () => {
|
||||||
|
const storeWithoutName = { ...mockStore, name: "" };
|
||||||
|
mockClient.vectorStores.retrieve.mockResolvedValue(storeWithoutName);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const storeIdTexts = screen.getAllByText("vs_123");
|
||||||
|
expect(storeIdTexts.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Sidebar Properties", () => {
|
||||||
|
test.skip("renders file and store properties correctly", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("File ID")).toBeInTheDocument();
|
||||||
|
const fileIdTexts = screen.getAllByText("file_456");
|
||||||
|
expect(fileIdTexts.length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("Vector Store ID")).toBeInTheDocument();
|
||||||
|
const storeIdTexts = screen.getAllByText("vs_123");
|
||||||
|
expect(storeIdTexts.length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("Status")).toBeInTheDocument();
|
||||||
|
const completedTexts = screen.getAllByText("completed");
|
||||||
|
expect(completedTexts.length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("Usage Bytes")).toBeInTheDocument();
|
||||||
|
const usageTexts = screen.getAllByText("2048");
|
||||||
|
expect(usageTexts.length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("Content Strategy")).toBeInTheDocument();
|
||||||
|
const fixedSizeTexts = screen.getAllByText("fixed_size");
|
||||||
|
expect(fixedSizeTexts.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(screen.getByText("Store Name")).toBeInTheDocument();
|
||||||
|
const storeNameTexts = screen.getAllByText("Test Vector Store");
|
||||||
|
expect(storeNameTexts.length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("Provider ID")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("test_provider")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles missing optional properties", async () => {
|
||||||
|
const minimalFile = {
|
||||||
|
id: "file_456",
|
||||||
|
status: "completed",
|
||||||
|
created_at: 1710001000,
|
||||||
|
usage_bytes: 2048,
|
||||||
|
chunking_strategy: { type: "fixed_size" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const minimalStore = {
|
||||||
|
...mockStore,
|
||||||
|
name: "",
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockClient.vectorStores.files.retrieve.mockResolvedValue(minimalFile);
|
||||||
|
mockClient.vectorStores.retrieve.mockResolvedValue(minimalStore);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const fileIdTexts = screen.getAllByText("file_456");
|
||||||
|
expect(fileIdTexts.length).toBeGreaterThan(0);
|
||||||
|
const storeIdTexts = screen.getAllByText("vs_123");
|
||||||
|
expect(storeIdTexts.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("File: file_456")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Loading States for Individual Sections", () => {
|
||||||
|
test("shows loading skeleton for content while file loads", async () => {
|
||||||
|
mockClient.vectorStores.files.content.mockImplementation(
|
||||||
|
() => new Promise(() => {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { container } = render(<FileDetailPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Content Summary")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error Handling", () => {
|
||||||
|
test("handles multiple simultaneous errors gracefully", async () => {
|
||||||
|
mockClient.vectorStores.files.retrieve.mockRejectedValue(
|
||||||
|
new Error("File error")
|
||||||
|
);
|
||||||
|
mockClient.vectorStores.files.content.mockRejectedValue(
|
||||||
|
new Error("Content error")
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
render(<FileDetailPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("Error loading file: File error")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Error loading content summary: Content error")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -187,6 +187,7 @@ const COMPONENTS = {
|
||||||
code: ({
|
code: ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
...rest
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
|
@ -0,0 +1,315 @@
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import { VectorStoreDetailView } from "./vector-store-detail";
|
||||||
|
import type { VectorStore } from "llama-stack-client/resources/vector-stores/vector-stores";
|
||||||
|
import type { VectorStoreFile } from "llama-stack-client/resources/vector-stores/files";
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
jest.mock("next/navigation", () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("VectorStoreDetailView", () => {
|
||||||
|
const defaultProps = {
|
||||||
|
store: null,
|
||||||
|
files: [],
|
||||||
|
isLoadingStore: false,
|
||||||
|
isLoadingFiles: false,
|
||||||
|
errorStore: null,
|
||||||
|
errorFiles: null,
|
||||||
|
id: "test_vector_store_id",
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPush.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Loading States", () => {
|
||||||
|
test("renders loading skeleton when store is loading", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VectorStoreDetailView {...defaultProps} isLoadingStore={true} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders files loading skeleton when files are loading", () => {
|
||||||
|
const mockStore: VectorStore = {
|
||||||
|
id: "vs_123",
|
||||||
|
name: "Test Vector Store",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 5 },
|
||||||
|
usage_bytes: 1024,
|
||||||
|
metadata: {
|
||||||
|
provider_id: "test_provider",
|
||||||
|
provider_vector_db_id: "test_db_id",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<VectorStoreDetailView
|
||||||
|
{...defaultProps}
|
||||||
|
store={mockStore}
|
||||||
|
isLoadingFiles={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Files")).toBeInTheDocument();
|
||||||
|
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error States", () => {
|
||||||
|
test("renders error message when store error occurs", () => {
|
||||||
|
render(
|
||||||
|
<VectorStoreDetailView
|
||||||
|
{...defaultProps}
|
||||||
|
errorStore={{ name: "Error", message: "Failed to load store" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Error loading details for ID test_vector_store_id/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Failed to load store/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders files error when files fail to load", () => {
|
||||||
|
const mockStore: VectorStore = {
|
||||||
|
id: "vs_123",
|
||||||
|
name: "Test Vector Store",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 5 },
|
||||||
|
usage_bytes: 1024,
|
||||||
|
metadata: {
|
||||||
|
provider_id: "test_provider",
|
||||||
|
provider_vector_db_id: "test_db_id",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<VectorStoreDetailView
|
||||||
|
{...defaultProps}
|
||||||
|
store={mockStore}
|
||||||
|
errorFiles={{ name: "Error", message: "Failed to load files" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Files")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Error loading files: Failed to load files")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Not Found State", () => {
|
||||||
|
test("renders not found message when store is null", () => {
|
||||||
|
render(<VectorStoreDetailView {...defaultProps} store={null} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/No details found for ID: test_vector_store_id/)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Store Data Rendering", () => {
|
||||||
|
const mockStore: VectorStore = {
|
||||||
|
id: "vs_123",
|
||||||
|
name: "Test Vector Store",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 3 },
|
||||||
|
usage_bytes: 2048,
|
||||||
|
metadata: {
|
||||||
|
provider_id: "test_provider",
|
||||||
|
provider_vector_db_id: "test_db_id",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test("renders store properties correctly", () => {
|
||||||
|
render(<VectorStoreDetailView {...defaultProps} store={mockStore} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Vector Store Details")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("vs_123")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Test Vector Store")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(new Date(1710000000 * 1000).toLocaleString())
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("ready")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("3")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("2048")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("test_provider")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("test_db_id")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty/missing optional fields", () => {
|
||||||
|
const minimalStore: VectorStore = {
|
||||||
|
id: "vs_minimal",
|
||||||
|
name: "",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 0 },
|
||||||
|
usage_bytes: 0,
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<VectorStoreDetailView {...defaultProps} store={minimalStore} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("vs_minimal")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("ready")).toBeInTheDocument();
|
||||||
|
const zeroTexts = screen.getAllByText("0");
|
||||||
|
expect(zeroTexts.length).toBeGreaterThanOrEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows empty files message when no files", () => {
|
||||||
|
render(
|
||||||
|
<VectorStoreDetailView {...defaultProps} store={mockStore} files={[]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Files")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("No files in this vector store.")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Files Table", () => {
|
||||||
|
const mockStore: VectorStore = {
|
||||||
|
id: "vs_123",
|
||||||
|
name: "Test Vector Store",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 2 },
|
||||||
|
usage_bytes: 2048,
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFiles: VectorStoreFile[] = [
|
||||||
|
{
|
||||||
|
id: "file_123",
|
||||||
|
status: "completed",
|
||||||
|
created_at: 1710001000,
|
||||||
|
usage_bytes: 1024,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file_456",
|
||||||
|
status: "processing",
|
||||||
|
created_at: 1710002000,
|
||||||
|
usage_bytes: 512,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test("renders files table with correct data", () => {
|
||||||
|
render(
|
||||||
|
<VectorStoreDetailView
|
||||||
|
{...defaultProps}
|
||||||
|
store={mockStore}
|
||||||
|
files={mockFiles}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Files")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Files in this vector store")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("ID")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Status")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Created")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Usage Bytes")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("file_123")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("completed")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(new Date(1710001000 * 1000).toLocaleString())
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("1024")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("file_456")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("processing")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(new Date(1710002000 * 1000).toLocaleString())
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("512")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("file ID links are clickable and navigate correctly", () => {
|
||||||
|
render(
|
||||||
|
<VectorStoreDetailView
|
||||||
|
{...defaultProps}
|
||||||
|
store={mockStore}
|
||||||
|
files={mockFiles}
|
||||||
|
id="vs_123"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileButton = screen.getByRole("button", { name: "file_123" });
|
||||||
|
expect(fileButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(fileButton);
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/logs/vector-stores/vs_123/files/file_123"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles multiple file clicks correctly", () => {
|
||||||
|
render(
|
||||||
|
<VectorStoreDetailView
|
||||||
|
{...defaultProps}
|
||||||
|
store={mockStore}
|
||||||
|
files={mockFiles}
|
||||||
|
id="vs_123"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const file1Button = screen.getByRole("button", { name: "file_123" });
|
||||||
|
const file2Button = screen.getByRole("button", { name: "file_456" });
|
||||||
|
|
||||||
|
fireEvent.click(file1Button);
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/logs/vector-stores/vs_123/files/file_123"
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(file2Button);
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/logs/vector-stores/vs_123/files/file_456"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockPush).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Layout Structure", () => {
|
||||||
|
const mockStore: VectorStore = {
|
||||||
|
id: "vs_layout_test",
|
||||||
|
name: "Layout Test Store",
|
||||||
|
created_at: 1710000000,
|
||||||
|
status: "ready",
|
||||||
|
file_counts: { total: 1 },
|
||||||
|
usage_bytes: 1024,
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
test("renders main content and sidebar in correct layout", () => {
|
||||||
|
render(<VectorStoreDetailView {...defaultProps} store={mockStore} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Files")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("vs_layout_test")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Layout Test Store")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("ready")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("1024")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -111,6 +111,9 @@ echo "Inference Mode: $INFERENCE_MODE"
|
||||||
echo "Test Pattern: $TEST_PATTERN"
|
echo "Test Pattern: $TEST_PATTERN"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
echo "Checking llama packages"
|
||||||
|
uv pip list | grep llama
|
||||||
|
|
||||||
# Check storage and memory before tests
|
# Check storage and memory before tests
|
||||||
echo "=== System Resources Before Tests ==="
|
echo "=== System Resources Before Tests ==="
|
||||||
free -h 2>/dev/null || echo "free command not available"
|
free -h 2>/dev/null || echo "free command not available"
|
||||||
|
|
|
@ -133,24 +133,15 @@ def test_agent_simple(llama_stack_client, agent_config):
|
||||||
assert "I can't" in logs_str
|
assert "I can't" in logs_str
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="this test was disabled for a long time, and now has turned flaky")
|
||||||
def test_agent_name(llama_stack_client, text_model_id):
|
def test_agent_name(llama_stack_client, text_model_id):
|
||||||
agent_name = f"test-agent-{uuid4()}"
|
agent_name = f"test-agent-{uuid4()}"
|
||||||
|
|
||||||
try:
|
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
llama_stack_client,
|
llama_stack_client,
|
||||||
model=text_model_id,
|
model=text_model_id,
|
||||||
instructions="You are a helpful assistant",
|
instructions="You are a helpful assistant",
|
||||||
name=agent_name,
|
name=agent_name,
|
||||||
)
|
)
|
||||||
except TypeError:
|
|
||||||
agent = Agent(
|
|
||||||
llama_stack_client,
|
|
||||||
model=text_model_id,
|
|
||||||
instructions="You are a helpful assistant",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
session_id = agent.create_session(f"test-session-{uuid4()}")
|
session_id = agent.create_session(f"test-session-{uuid4()}")
|
||||||
|
|
||||||
agent.create_turn(
|
agent.create_turn(
|
||||||
|
|
Binary file not shown.
|
@ -14,7 +14,7 @@
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"model": "nomic-embed-text:latest",
|
"model": "nomic-embed-text:latest",
|
||||||
"modified_at": "2025-08-15T21:55:08.088554Z",
|
"modified_at": "2025-08-18T12:47:56.732989-07:00",
|
||||||
"digest": "0a109f422b47e3a30ba2b10eca18548e944e8a23073ee3f3e947efcf3c45e59f",
|
"digest": "0a109f422b47e3a30ba2b10eca18548e944e8a23073ee3f3e947efcf3c45e59f",
|
||||||
"size": 274302450,
|
"size": 274302450,
|
||||||
"details": {
|
"details": {
|
||||||
|
@ -28,9 +28,41 @@
|
||||||
"quantization_level": "F16"
|
"quantization_level": "F16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"model": "llama3.2-vision:11b",
|
||||||
|
"modified_at": "2025-07-30T18:45:02.517873-07:00",
|
||||||
|
"digest": "6f2f9757ae97e8a3f8ea33d6adb2b11d93d9a35bef277cd2c0b1b5af8e8d0b1e",
|
||||||
|
"size": 7816589186,
|
||||||
|
"details": {
|
||||||
|
"parent_model": "",
|
||||||
|
"format": "gguf",
|
||||||
|
"family": "mllama",
|
||||||
|
"families": [
|
||||||
|
"mllama"
|
||||||
|
],
|
||||||
|
"parameter_size": "10.7B",
|
||||||
|
"quantization_level": "Q4_K_M"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "llama3.2-vision:latest",
|
||||||
|
"modified_at": "2025-07-29T20:18:47.920468-07:00",
|
||||||
|
"digest": "6f2f9757ae97e8a3f8ea33d6adb2b11d93d9a35bef277cd2c0b1b5af8e8d0b1e",
|
||||||
|
"size": 7816589186,
|
||||||
|
"details": {
|
||||||
|
"parent_model": "",
|
||||||
|
"format": "gguf",
|
||||||
|
"family": "mllama",
|
||||||
|
"families": [
|
||||||
|
"mllama"
|
||||||
|
],
|
||||||
|
"parameter_size": "10.7B",
|
||||||
|
"quantization_level": "Q4_K_M"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"model": "llama-guard3:1b",
|
"model": "llama-guard3:1b",
|
||||||
"modified_at": "2025-07-31T04:44:58Z",
|
"modified_at": "2025-07-25T14:39:44.978630-07:00",
|
||||||
"digest": "494147e06bf99e10dbe67b63a07ac81c162f18ef3341aa3390007ac828571b3b",
|
"digest": "494147e06bf99e10dbe67b63a07ac81c162f18ef3341aa3390007ac828571b3b",
|
||||||
"size": 1600181919,
|
"size": 1600181919,
|
||||||
"details": {
|
"details": {
|
||||||
|
@ -46,7 +78,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model": "all-minilm:l6-v2",
|
"model": "all-minilm:l6-v2",
|
||||||
"modified_at": "2025-07-31T04:42:15Z",
|
"modified_at": "2025-07-24T15:15:11.129290-07:00",
|
||||||
"digest": "1b226e2802dbb772b5fc32a58f103ca1804ef7501331012de126ab22f67475ef",
|
"digest": "1b226e2802dbb772b5fc32a58f103ca1804ef7501331012de126ab22f67475ef",
|
||||||
"size": 45960996,
|
"size": 45960996,
|
||||||
"details": {
|
"details": {
|
||||||
|
@ -60,9 +92,57 @@
|
||||||
"quantization_level": "F16"
|
"quantization_level": "F16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"model": "llama3.2:1b",
|
||||||
|
"modified_at": "2025-07-17T22:02:24.953208-07:00",
|
||||||
|
"digest": "baf6a787fdffd633537aa2eb51cfd54cb93ff08e28040095462bb63daf552878",
|
||||||
|
"size": 1321098329,
|
||||||
|
"details": {
|
||||||
|
"parent_model": "",
|
||||||
|
"format": "gguf",
|
||||||
|
"family": "llama",
|
||||||
|
"families": [
|
||||||
|
"llama"
|
||||||
|
],
|
||||||
|
"parameter_size": "1.2B",
|
||||||
|
"quantization_level": "Q8_0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "all-minilm:latest",
|
||||||
|
"modified_at": "2025-06-03T16:50:10.946583-07:00",
|
||||||
|
"digest": "1b226e2802dbb772b5fc32a58f103ca1804ef7501331012de126ab22f67475ef",
|
||||||
|
"size": 45960996,
|
||||||
|
"details": {
|
||||||
|
"parent_model": "",
|
||||||
|
"format": "gguf",
|
||||||
|
"family": "bert",
|
||||||
|
"families": [
|
||||||
|
"bert"
|
||||||
|
],
|
||||||
|
"parameter_size": "23M",
|
||||||
|
"quantization_level": "F16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "llama3.2:3b",
|
||||||
|
"modified_at": "2025-05-01T11:15:23.797447-07:00",
|
||||||
|
"digest": "a80c4f17acd55265feec403c7aef86be0c25983ab279d83f3bcd3abbcb5b8b72",
|
||||||
|
"size": 2019393189,
|
||||||
|
"details": {
|
||||||
|
"parent_model": "",
|
||||||
|
"format": "gguf",
|
||||||
|
"family": "llama",
|
||||||
|
"families": [
|
||||||
|
"llama"
|
||||||
|
],
|
||||||
|
"parameter_size": "3.2B",
|
||||||
|
"quantization_level": "Q4_K_M"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"model": "llama3.2:3b-instruct-fp16",
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
"modified_at": "2025-07-31T04:42:05Z",
|
"modified_at": "2025-04-30T15:33:48.939665-07:00",
|
||||||
"digest": "195a8c01d91ec3cb1e0aad4624a51f2602c51fa7d96110f8ab5a20c84081804d",
|
"digest": "195a8c01d91ec3cb1e0aad4624a51f2602c51fa7d96110f8ab5a20c84081804d",
|
||||||
"size": 6433703586,
|
"size": 6433703586,
|
||||||
"details": {
|
"details": {
|
||||||
|
|
203
tests/integration/recordings/responses/731824c54461.json
Normal file
203
tests/integration/recordings/responses/731824c54461.json
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost:11434/api/generate",
|
||||||
|
"headers": {},
|
||||||
|
"body": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"raw": true,
|
||||||
|
"prompt": "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\nYou are a helpful assistant<|eot_id|><|start_header_id|>user<|end_header_id|>\n\nGive me a sentence that contains the word: hello<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n",
|
||||||
|
"options": {
|
||||||
|
"temperature": 0.0
|
||||||
|
},
|
||||||
|
"stream": true
|
||||||
|
},
|
||||||
|
"endpoint": "/api/generate",
|
||||||
|
"model": "llama3.2:3b-instruct-fp16"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.267146Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": "Hello",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.309006Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": ",",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.351179Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": " how",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.393262Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": " can",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.436079Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": " I",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.478393Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": " assist",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.520608Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": " you",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.562885Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": " today",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.604683Z",
|
||||||
|
"done": false,
|
||||||
|
"done_reason": null,
|
||||||
|
"total_duration": null,
|
||||||
|
"load_duration": null,
|
||||||
|
"prompt_eval_count": null,
|
||||||
|
"prompt_eval_duration": null,
|
||||||
|
"eval_count": null,
|
||||||
|
"eval_duration": null,
|
||||||
|
"response": "?",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"__type__": "ollama._types.GenerateResponse",
|
||||||
|
"__data__": {
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"created_at": "2025-08-18T19:47:58.646586Z",
|
||||||
|
"done": true,
|
||||||
|
"done_reason": "stop",
|
||||||
|
"total_duration": 1011323917,
|
||||||
|
"load_duration": 76575458,
|
||||||
|
"prompt_eval_count": 31,
|
||||||
|
"prompt_eval_duration": 553259250,
|
||||||
|
"eval_count": 10,
|
||||||
|
"eval_duration": 380302792,
|
||||||
|
"response": "",
|
||||||
|
"thinking": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_streaming": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,26 @@
|
||||||
"body": {
|
"body": {
|
||||||
"__type__": "ollama._types.ProcessResponse",
|
"__type__": "ollama._types.ProcessResponse",
|
||||||
"__data__": {
|
"__data__": {
|
||||||
"models": []
|
"models": [
|
||||||
|
{
|
||||||
|
"model": "llama3.2:3b-instruct-fp16",
|
||||||
|
"name": "llama3.2:3b-instruct-fp16",
|
||||||
|
"digest": "195a8c01d91ec3cb1e0aad4624a51f2602c51fa7d96110f8ab5a20c84081804d",
|
||||||
|
"expires_at": "2025-08-18T13:47:44.262256-07:00",
|
||||||
|
"size": 7919570944,
|
||||||
|
"size_vram": 7919570944,
|
||||||
|
"details": {
|
||||||
|
"parent_model": "",
|
||||||
|
"format": "gguf",
|
||||||
|
"family": "llama",
|
||||||
|
"families": [
|
||||||
|
"llama"
|
||||||
|
],
|
||||||
|
"parameter_size": "3.2B",
|
||||||
|
"quantization_level": "F16"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is_streaming": false
|
"is_streaming": false
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue