package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"slices"
	"strings"
)

var (
	ErrExpressionOrListMustBeStringOrObject = errors.New("config: this must be a string or an object")
	ErrExpressionEmpty                      = errors.New("config: this expression is empty")
	ErrExpressionCantHaveBoth               = errors.New("config: expression block can't contain multiple expression types")
)

type ExpressionOrList struct {
	Expression string   `json:"-" yaml:"-"`
	All        []string `json:"all,omitempty" yaml:"all,omitempty"`
	Any        []string `json:"any,omitempty" yaml:"any,omitempty"`
}

func (eol ExpressionOrList) String() string {
	switch {
	case len(eol.Expression) != 0:
		return eol.Expression
	case len(eol.All) != 0:
		var sb strings.Builder
		for i, pred := range eol.All {
			if i != 0 {
				fmt.Fprintf(&sb, " && ")
			}
			fmt.Fprintf(&sb, "( %s )", pred)
		}
		return sb.String()
	case len(eol.Any) != 0:
		var sb strings.Builder
		for i, pred := range eol.Any {
			if i != 0 {
				fmt.Fprintf(&sb, " || ")
			}
			fmt.Fprintf(&sb, "( %s )", pred)
		}
		return sb.String()
	}
	panic("this should not happen")
}

func (eol ExpressionOrList) Equal(rhs *ExpressionOrList) bool {
	if eol.Expression != rhs.Expression {
		return false
	}

	if !slices.Equal(eol.All, rhs.All) {
		return false
	}

	if !slices.Equal(eol.Any, rhs.Any) {
		return false
	}

	return true
}

func (eol *ExpressionOrList) MarshalYAML() (any, error) {
	switch {
	case len(eol.All) == 1 && len(eol.Any) == 0:
		eol.Expression = eol.All[0]
		eol.All = nil
	case len(eol.Any) == 1 && len(eol.All) == 0:
		eol.Expression = eol.Any[0]
		eol.Any = nil
	}

	if eol.Expression != "" {
		return eol.Expression, nil
	}

	type RawExpressionOrList ExpressionOrList
	return RawExpressionOrList(*eol), nil
}

func (eol *ExpressionOrList) MarshalJSON() ([]byte, error) {
	switch {
	case len(eol.All) == 1 && len(eol.Any) == 0:
		eol.Expression = eol.All[0]
		eol.All = nil
	case len(eol.Any) == 1 && len(eol.All) == 0:
		eol.Expression = eol.Any[0]
		eol.Any = nil
	}

	if eol.Expression != "" {
		return json.Marshal(string(eol.Expression))
	}

	type RawExpressionOrList ExpressionOrList
	val := RawExpressionOrList(*eol)
	return json.Marshal(val)
}

func (eol *ExpressionOrList) UnmarshalJSON(data []byte) error {
	switch string(data[0]) {
	case `"`: // string
		return json.Unmarshal(data, &eol.Expression)
	case "{": // object
		type RawExpressionOrList ExpressionOrList
		var val RawExpressionOrList
		if err := json.Unmarshal(data, &val); err != nil {
			return err
		}
		eol.All = val.All
		eol.Any = val.Any

		return nil
	}

	return ErrExpressionOrListMustBeStringOrObject
}

func (eol *ExpressionOrList) Valid() error {
	if eol.Expression == "" && len(eol.All) == 0 && len(eol.Any) == 0 {
		return ErrExpressionEmpty
	}
	if len(eol.All) != 0 && len(eol.Any) != 0 {
		return ErrExpressionCantHaveBoth
	}

	return nil
}


package ogtags

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

	"github.com/TecharoHQ/anubis/lib/config"
	"github.com/TecharoHQ/anubis/lib/store/memory"
)

//nolint:errcheck
func TestIntegrationGetOGTags(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")

		switch r.URL.Path {
		case "/simple":
			w.Write([]byte(`
				<!DOCTYPE html>

					<meta property="og:title" content="Simple Page" />
					<meta property="og:type" content="website" />

Simple page content

			`))
		case "/complete":
			w.Write([]byte(`
				<!DOCTYPE html>

					<meta property="og:title" content="Complete Page" />
					<meta property="og:description" content="A page with many OG tags" />
					<meta property="og:image" content="http://example.com/image.jpg" />
					<meta property="og:url" content="http://example.com/complete" />
					<meta property="og:type" content="article" />

Complete page content

			`))
		case "/no-og":
			w.Write([]byte(`
				<!DOCTYPE html>

					<title>No OG Tags</title>

No OG tags here

			`))
		default:
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer ts.Close()

	// Test with different configurations
	testCases := []struct {
		expectedTags map[string]string
		name         string
		path         string
		query        string
		expectError  bool
	}{
		{
			name:  "Simple page",
			path:  "/simple",
			query: "",
			expectedTags: map[string]string{
				"og:title": "Simple Page",
				"og:type":  "website",
			},
			expectError: false,
		},
		{
			name:  "Complete page",
			path:  "/complete",
			query: "ref=test",
			expectedTags: map[string]string{
				"og:title":       "Complete Page",
				"og:description": "A page with many OG tags",
				"og:image":       "http://example.com/image.jpg",
				"og:url":         "http://example.com/complete",
				"og:type":        "article",
			},
			expectError: false,
		},
		{
			name:         "Page with no OG tags",
			path:         "/no-og",
			query:        "",
			expectedTags: map[string]string{},
			expectError:  false,
		},
		{
			name:         "Nonexistent page",
			path:         "/not-found",
			query:        "",
			expectedTags: nil,
			expectError:  false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Create cache instance
			cache := NewOGTagCache(ts.URL, config.OpenGraph{
				Enabled:      true,
				TimeToLive:   time.Minute,
				ConsiderHost: false,
			}, memory.New(t.Context()), TargetOptions{})

			// Create URL for test
			testURL, _ := url.Parse(ts.URL)
			testURL.Path = tc.path
			testURL.RawQuery = tc.query

			// Get OG tags
			// Pass the host from the test URL
			ogTags, err := cache.GetOGTags(t.Context(), testURL, testURL.Host)

			// Check error expectation
			if tc.expectError {
				if err == nil {
					t.Error("expected error, got nil")
				}
				return
			}

			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			// Verify all expected tags are present
			for key, expectedValue := range tc.expectedTags {
				if value, ok := ogTags[key]; !ok || value != expectedValue {
					t.Errorf("expected %s: %s, got: %s", key, expectedValue, value)
				}
			}

			// Verify no extra tags are present
			if len(ogTags) != len(tc.expectedTags) {
				t.Errorf("expected %d tags, got %d", len(tc.expectedTags), len(ogTags))
			}

			// Test cache retrieval
			// Pass the host from the test URL
			cachedOGTags, err := cache.GetOGTags(t.Context(), testURL, testURL.Host)
			if err != nil {
				t.Fatalf("failed to get OG tags from cache: %v", err)
			}

			// Verify cached tags match
			for key, expectedValue := range tc.expectedTags {
				if value, ok := cachedOGTags[key]; !ok || value != expectedValue {
					t.Errorf("cached value - expected %s: %s, got: %s", key, expectedValue, value)
				}
			}
		})
	}
}


// Copyright (c) Epic Games Tools
// Licensed under the MIT license (https://opensource.org/license/mit/)

internal B32
w32_x64_write_reg_block_from_thread_ctx(void *reg_block, void *thread_ctx)
{
  X64_RegBlock *dst = (X64_RegBlock *)reg_block;
  W32_X64_ThreadContext *src = (W32_X64_ThreadContext *)thread_ctx;

  //- rjf: convert context -> X64_RegBlock
  W32_X64_XSaveFormat *xsave = &src->FltSave;
  dst->rax = src->Rax;
  dst->rcx = src->Rcx;
  dst->rdx = src->Rdx;
  dst->rbx = src->Rbx;
  dst->rsp = src->Rsp;
  dst->rbp = src->Rbp;
  dst->rsi = src->Rsi;
  dst->rdi = src->Rdi;
  dst->r8  = src->R8;
  dst->r9  = src->R9;
  dst->r10 = src->R10;
  dst->r11 = src->R11;
  dst->r12 = src->R12;
  dst->r13 = src->R13;
  dst->r14 = src->R14;
  dst->r15 = src->R15;
  dst->rip = src->Rip;
  dst->cs  = src->SegCs;
  dst->ds  = src->SegDs;
  dst->es  = src->SegEs;
  dst->fs  = src->SegFs;
  dst->gs  = src->SegGs;
  dst->ss  = src->SegSs;
  dst->dr0 = src->Dr0;
  dst->dr1 = src->Dr1;
  dst->dr2 = src->Dr2;
  dst->dr3 = src->Dr3;
  dst->dr6 = src->Dr6;
  dst->dr7 = src->Dr7;
  // NOTE(rjf): this bit is "supposed to always be 1", according to old info.
  // may need to be investigated.
  dst->rflags = src->EFlags | 0x2;
  dst->fcw = xsave->ControlWord;
  dst->fsw = xsave->StatusWord;
  dst->ftw = xsave->TagWord;
  dst->fop = xsave->ErrorOpcode;
  MemoryCopy(&dst->fip, &xsave->ErrorOffset, sizeof(U64));
  MemoryCopy(&dst->fdp, &xsave->DataOffset, sizeof(U64));
  dst->mxcsr = xsave->MxCsr;
  dst->mxcsr_mask = xsave->MxCsr_Mask;
  {
    U128 *float_s = xsave->FloatRegisters;
    U80 *float_d = &dst->st0;
    for(U32 n = 0; n < 8; n += 1, float_s += 1, float_d += 1)
    {
      MemoryCopy(float_d, float_s, sizeof(*float_d));
    }
  }
  {
    U128 *xmm_s = xsave->XmmRegisters;
    U512 *zmm_d = &dst->zmm0;
    for(U32 n = 0; n < 16; n += 1, xmm_s += 1, zmm_d += 1)
    {
      MemoryCopy(zmm_d, xmm_s, sizeof(*xmm_s));
    }
  }

  // TODO(rjf): we need to determine how to do LocateXStateFeature without
  // actually running on Windows - what is that function actually looking at
  // & doing?
#if 0
  // AVX
  {
    DWORD avx_length = 0;
    U8 *avx_s = (U8 *)LocateXStateFeature(ctx, XSTATE_AVX, &avx_length);
    if(avx_length == 16 * sizeof(U128))
    {
      U512 *zmm_d = &dst->zmm0;
      for(U32 n = 0; n < 16; n += 1, avx_s += sizeof(U128), zmm_d += 1)
      {
        MemoryCopy(&zmm_d->u8[16], avx_s, sizeof(U128));
      }
    }
  }

  // AVX-512
  {
    // rjf: kmask
    DWORD kmask_length = 0;
    U64 *kmask_s = (U64*)LocateXStateFeature(ctx, XSTATE_AVX512_KMASK, &kmask_length);
    if(kmask_length == 8 * sizeof(U64))
    {
      U64 *kmask_d = &dst->k0;
      for(U32 n = 0; n < 8; n += 1, kmask_s += 1, kmask_d += 1)
      {
        MemoryCopy(kmask_d, kmask_s, sizeof(*kmask_s));
      }
    }

    // rjf: zmmh
    DWORD avx512h_length = 0;
    U8 *avx512h_s = (U8*)LocateXStateFeature(ctx, XSTATE_AVX512_ZMM_H, &avx512h_length);
    if(avx512h_length == 16 * sizeof(U256))
    {
      U512 *zmmh_d = &dst->zmm0;
      for(U32 n = 0; n < 16; n += 1, avx512h_s += sizeof(U256), zmmh_d += 1)
      {
        MemoryCopy(&zmmh_d->u8[32], avx512h_s, sizeof(U256));
      }
    }

    // rjf: zmm
    DWORD avx512_length = 0;
    U8 *avx512_s = (U8 *)LocateXStateFeature(ctx, XSTATE_AVX512_ZMM, &avx512_length);
    if(avx512_length == 16 * sizeof(U512))
    {
      U512 *zmm_d = &dst->zmm16;
      for(U32 n = 0; n < 16; n += 1, avx512_s += sizeof(U512), zmm_d += 1)
      {
        MemoryCopy(zmm_d, avx512_s, sizeof(U512));
      }
    }
  }

  // CET / Shadow Stack
  if(xstate_mask & XSTATE_MASK_CET_U)
  {
    DWORD cet_length = 0;
    XSAVE_CET_U_FORMAT *cet = LocateXStateFeature(ctx, XSTATE_CET_U, &cet_length);
    if (cet_length == sizeof(*cet))
    {
      dst->cetmsr = cet->Ia32CetUMsr;
      dst->cetssp = cet->Ia32Pl3SspMsr;
    }
  }
#endif
  return 1;
}


#!/usr/bin/env node
// Shared Ponytail instruction builder for Claude hooks and Pi extension.

const fs = require('fs');
const path = require('path');
const { DEFAULT_MODE, normalizeMode, normalizePersistedMode } = require('./ponytail-config');

const INDEPENDENT_MODES = new Set(['review']);
const SKILL_PATH = path.join(__dirname, '..', 'skills', 'ponytail', 'SKILL.md');

function filterSkillBodyForMode(body, mode) {
  const effectiveMode = normalizeMode(mode) || DEFAULT_MODE;
  const withoutFrontmatter = String(body || '').replace(/^---[\s\S]*?---\s*/, '');

  // Only the intensity table rows and worked examples are mode-specific, and
  // both are keyed by a mode name (lite/full/ultra). A bullet whose label is
  // not a mode — e.g. "No unrequested abstractions: ..." — is a normal rule
  // and must be kept verbatim.
  return withoutFrontmatter
    .split(/\r?\n/)
    .filter((line) => {
      const tableLabel = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/);
      if (tableLabel) {
        const labelMode = normalizeMode(tableLabel[1].trim());
        if (labelMode) return labelMode === effectiveMode;
      }

      // Require a quoted value: every worked example is `- lite: "..."`. Without
      // this, an ordinary rule bullet that happens to start with a mode word
      // (e.g. "- Full: ...") is silently dropped in every other mode — it looks
      // like a worked example but is really prose meant to survive verbatim.
      const exampleLabel = line.match(/^-\s*([^:]+):\s*"/);
      if (exampleLabel) {
        const labelMode = normalizeMode(exampleLabel[1].trim());
        if (labelMode) return labelMode === effectiveMode;
      }

      return true;
    })
    .join('\n');
}

function getFallbackInstructions(mode) {
  return 'PONYTAIL MODE ACTIVE — level: ' + mode + '\n\n' +
    'You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.\n\n' +
    '## Persistence\n\n' +
    'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\n\n' +
    'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra`.\n\n' +
    '## The ladder\n\n' +
    'Before any code, stop at the first rung that holds (the ladder runs after you understand the problem, not instead of it — read the code it touches and trace the real flow first):\n' +
    '1. Does this need to be built at all? (YAGNI)\n' +
    '2. Does it already exist in this codebase? Reuse what is already here, do not re-write it.\n' +
    '3. Does the standard library do this? Use it.\n' +
    '4. Does a native platform feature cover it? Use it.\n' +
    '5. Does an already-installed dependency solve it? Use it.\n' +
    '6. Can this be one line? Make it one line.\n' +
    '7. Only then: write the minimum code that works.\n\n' +
    'Bug fix = root cause, not symptom: grep every caller of the function you touch and fix the shared function once (a smaller diff than one guard per caller); patching only the path the ticket names leaves a sibling caller broken.\n\n' +
    '## Rules\n\n' +
    'No abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. ' +
    'Deletion over addition. Boring over clever. Fewest files possible. ' +
    'Ship the lazy version and question the complex request in the same response — never stall. ' +
    'Between two same-size stdlib options, pick the one correct on edge cases. ' +
    'Mark deliberate simplifications that cut a real corner with a known ceiling, using a `ponytail:` comment that names the ceiling and upgrade path.\n\n' +
    '## Output\n\n' +
    'Code first. Then at most three short lines: what was skipped, when to add it. ' +
    'If the explanation is longer than the code, delete the explanation. ' +
    'Explanation the user explicitly asked for is not debt, give it in full.\n\n' +
    '## When NOT to be lazy\n\n' +
    'Never simplify away: understanding the problem (read it fully and trace the real flow before picking a rung — a small diff you do not understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, ' +
    'security measures, accessibility basics, the calibration real hardware needs (the platform is never the spec ideal), anything the user explicitly asked to keep. ' +
    'Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind (assert-based demo/self-check or one small test file; no frameworks). Trivial one-liners need no test.\n\n' +
    '## Boundaries\n\n' +
    'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.';
}

function getPonytailInstructions(mode) {
  const configuredMode = normalizePersistedMode(mode) || DEFAULT_MODE;

  if (INDEPENDENT_MODES.has(configuredMode)) {
    return 'PONYTAIL MODE ACTIVE — level: ' + configuredMode + '. Behavior defined by /ponytail-' + configuredMode + ' skill.';
  }

  const effectiveMode = normalizeMode(configuredMode) || DEFAULT_MODE;

  try {
    return 'PONYTAIL MODE ACTIVE — level: ' + effectiveMode + '\n\n' +
      filterSkillBodyForMode(fs.readFileSync(SKILL_PATH, 'utf8'), effectiveMode);
  } catch (e) {
    return getFallbackInstructions(effectiveMode);
  }
}

module.exports = {
  filterSkillBodyForMode,
  getFallbackInstructions,
  getPonytailInstructions,
};


"""
Tests for multi_act() page-change guards.

Verifies:
1. Metadata: terminates_sequence flags are set correctly on built-in actions
2. Static guard: actions tagged terminates_sequence abort remaining queued actions
3. Runtime guard: URL/focus changes detected after click-on-link abort remaining actions
4. Safe chain: multiple inputs execute without interruption

Usage:
	uv run pytest tests/ci/test_multi_act_guards.py -v -s
"""

import asyncio

import pytest
from pytest_httpserver import HTTPServer

from browser_use.agent.service import Agent
from browser_use.browser import BrowserSession
from browser_use.browser.profile import BrowserProfile
from browser_use.tools.service import Tools
from tests.ci.conftest import create_mock_llm

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(scope='session')
def http_server():
	"""Test HTTP server with pages for guard tests."""
	server = HTTPServer()
	server.start()

	server.expect_request('/form').respond_with_data(
		"""<title>Form Page</title>

Form

		<input id="field1" type="text" placeholder="Field 1" />
		<input id="field2" type="text" placeholder="Field 2" />
		<input id="field3" type="text" placeholder="Field 3" />
		<button id="submit" type="submit">Submit</button>
		""",
		content_type='text/html',
	)

	server.expect_request('/page_a').respond_with_data(
		"""<title>Page A</title>

Page A

		Go to Page B
		""",
		content_type='text/html',
	)

	server.expect_request('/page_b').respond_with_data(
		"""<title>Page B</title>

Page B

You arrived at Page B

		""",
		content_type='text/html',
	)

	server.expect_request('/static').respond_with_data(
		"""<title>Static Page</title>

Static

Nothing changes here

		<input id="safe_input" type="text" />
		""",
		content_type='text/html',
	)

	yield server
	server.stop()

@pytest.fixture(scope='session')
def base_url(http_server):
	return f'http://{http_server.host}:{http_server.port}'

@pytest.fixture(scope='module')
async def browser_session():
	session = BrowserSession(
		browser_profile=BrowserProfile(
			headless=True,
			user_data_dir=None,
			keep_alive=True,
		)
	)
	await session.start()
	yield session
	await session.kill()
	await session.event_bus.stop(clear=True, timeout=5)

@pytest.fixture(scope='function')
def tools():
	return Tools()

# ---------------------------------------------------------------------------
# 1. Metadata tests — verify terminates_sequence flags
# ---------------------------------------------------------------------------

class TestTerminatesSequenceMetadata:
	"""Verify that built-in actions have correct terminates_sequence flags."""

	def test_navigate_terminates(self, tools):
		action = tools.registry.registry.actions.get('navigate')
		assert action is not None
		assert action.terminates_sequence is True

	def test_search_terminates(self, tools):
		action = tools.registry.registry.actions.get('search')
		assert action is not None
		assert action.terminates_sequence is True

	def test_go_back_terminates(self, tools):
		action = tools.registry.registry.actions.get('go_back')
		assert action is not None
		assert action.terminates_sequence is True

	def test_switch_terminates(self, tools):
		action = tools.registry.registry.actions.get('switch')
		assert action is not None
		assert action.terminates_sequence is True

	def test_click_does_not_terminate(self, tools):
		action = tools.registry.registry.actions.get('click')
		assert action is not None
		assert action.terminates_sequence is False

	def test_input_does_not_terminate(self, tools):
		action = tools.registry.registry.actions.get('input')
		assert action is not None
		assert action.terminates_sequence is False

	def test_scroll_does_not_terminate(self, tools):
		action = tools.registry.registry.actions.get('scroll')
		assert action is not None
		assert action.terminates_sequence is False

	def test_extract_does_not_terminate(self, tools):
		action = tools.registry.registry.actions.get('extract')
		assert action is not None
		assert action.terminates_sequence is False

	def test_evaluate_terminates(self, tools):
		"""evaluate() can mutate the DOM in unpredictable ways (e.g. dismiss cookie overlays),
		so any actions queued after it should be skipped to avoid stale element references."""
		action = tools.registry.registry.actions.get('evaluate')
		assert action is not None
		assert action.terminates_sequence is True

# ---------------------------------------------------------------------------
# 2. Static guard — navigate as non-last action skips remaining
# ---------------------------------------------------------------------------

class TestStaticGuard:
	"""Verify that terminates_sequence actions abort the remaining queue."""

	async def test_navigate_aborts_remaining_actions(self, browser_session, base_url, tools):
		"""When navigate is action 2/3, action 3 should never execute."""
		# Start on a known page
		await tools.navigate(url=f'{base_url}/static', new_tab=False, browser_session=browser_session)
		await asyncio.sleep(0.5)

		# Build action models: [scroll_down, navigate_to_page_a, scroll_down]
		ActionModel = tools.registry.create_action_model()
		actions = [
			ActionModel.model_validate({'scroll': {'down': True, 'pages': 1}}),
			ActionModel.model_validate({'navigate': {'url': f'{base_url}/page_a'}}),
			ActionModel.model_validate({'scroll': {'down': True, 'pages': 1}}),
		]

		mock_llm = create_mock_llm()
		agent = Agent(task='test', llm=mock_llm, browser_session=browser_session, tools=tools)

		results = await agent.multi_act(actions)

		# Should have executed exactly 2 actions (scroll + navigate), third skipped
		assert len(results) == 2, f'Expected 2 results but got {len(results)}: {results}'

		# Verify we actually navigated
		url = await browser_session.get_current_page_url()
		assert '/page_a' in url

	async def test_go_back_aborts_remaining_actions(self, browser_session, base_url, tools):
		"""go_back should abort remaining queued actions."""
		# Navigate to page_a then page_b so go_back has somewhere to go
		await tools.navigate(url=f'{base_url}/page_a', new_tab=False, browser_session=browser_session)
		await asyncio.sleep(0.3)
		await tools.navigate(url=f'{base_url}/page_b', new_tab=False, browser_session=browser_session)
		await asyncio.sleep(0.3)

		ActionModel = tools.registry.create_action_model()
		actions = [
			ActionModel.model_validate({'go_back': {}}),
			ActionModel.model_validate({'scroll': {'down': True, 'pages': 1}}),
		]

		mock_llm = create_mock_llm()
		agent = Agent(task='test', llm=mock_llm, browser_session=browser_session, tools=tools)

		results = await agent.multi_act(actions)

		# go_back should terminate the sequence — only 1 result
		assert len(results) == 1, f'Expected 1 result but got {len(results)}: {results}'

# ---------------------------------------------------------------------------
# 3. Runtime guard — click on link changes URL, remaining actions skipped
# ---------------------------------------------------------------------------

class TestRuntimeGuard:
	"""Verify that URL/focus changes detected at runtime abort remaining actions."""

	async def test_click_link_aborts_remaining(self, browser_session, base_url, tools):
		"""Click a link that navigates to another page — remaining actions skipped."""
		await tools.navigate(url=f'{base_url}/page_a', new_tab=False, browser_session=browser_session)
		await asyncio.sleep(0.5)

		# Get the selector map to find the link index
		state = await browser_session.get_browser_state_summary()
		assert state.dom_state is not None
		selector_map = state.dom_state.selector_map

		# Find the link element (a#link_b)
		link_index = None
		for idx, element in selector_map.items():
			if hasattr(element, 'tag_name') and element.tag_name == 'a':
				link_index = idx
				break

		assert link_index is not None, 'Could not find link element in selector map'

		ActionModel = tools.registry.create_action_model()
		actions = [
			ActionModel.model_validate({'click': {'index': link_index}}),
			ActionModel.model_validate({'scroll': {'down': True, 'pages': 1}}),
			ActionModel.model_validate({'scroll': {'down': True, 'pages': 1}}),
		]

		mock_llm = create_mock_llm()
		agent = Agent(task='test', llm=mock_llm, browser_session=browser_session, tools=tools)

		results = await agent.multi_act(actions)

		# Click navigated to page_b — runtime guard should stop at 1
		assert len(results) == 1, f'Expected 1 result but got {len(results)}: {results}'

		# Verify we're on page_b
		url = await browser_session.get_current_page_url()
		assert '/page_b' in url

# ---------------------------------------------------------------------------
# 4. Safe chain — multiple non-page-changing actions all execute
# ---------------------------------------------------------------------------

class TestSafeChain:
	"""Verify that non-page-changing actions execute without interruption."""

	async def test_multiple_scrolls_all_execute(self, browser_session, base_url, tools):
		"""Multiple scroll actions should all execute."""
		await tools.navigate(url=f'{base_url}/static', new_tab=False, browser_session=browser_session)
		await asyncio.sleep(0.5)

		ActionModel = tools.registry.create_action_model()
		actions = [
			ActionModel.model_validate({'scroll': {'down': True, 'pages': 0.5}}),
			ActionModel.model_validate({'scroll': {'down': True, 'pages': 0.5}}),
			ActionModel.model_validate({'scroll': {'down': False, 'pages': 0.5}}),
		]

		mock_llm = create_mock_llm()
		agent = Agent(task='test', llm=mock_llm, browser_session=browser_session, tools=tools)

		results = await agent.multi_act(actions)

		# All 3 scrolls should execute
		assert len(results) == 3, f'Expected 3 results but got {len(results)}: {results}'
		# None should have errors
		for r in results:
			assert r.error is None, f'Unexpected error: {r.error}'


import zipfile
import io
import os

from typing import BinaryIO, Any, TYPE_CHECKING

from .._base_converter import DocumentConverter, DocumentConverterResult
from .._stream_info import StreamInfo
from .._exceptions import UnsupportedFormatException, FileConversionException

# Break otherwise circular import for type hinting
if TYPE_CHECKING:
    from .._markitdown import MarkItDown

ACCEPTED_MIME_TYPE_PREFIXES = [
    "application/zip",
]

ACCEPTED_FILE_EXTENSIONS = [".zip"]

class ZipConverter(DocumentConverter):
    """Converts ZIP files to markdown by extracting and converting all contained files.

    The converter extracts the ZIP contents to a temporary directory, processes each file
    using appropriate converters based on file extensions, and then combines the results
    into a single markdown document. The temporary directory is cleaned up after processing.

    Example output format:
    ```markdown
    Content from the zip file `example.zip`:

    ## File: docs/readme.txt

    This is the content of readme.txt
    Multiple lines are preserved

    ## File: images/example.jpg

    ImageSize: 1920x1080
    DateTimeOriginal: 2024-02-15 14:30:00
    Description: A beautiful landscape photo

    ## File: data/report.xlsx

    ## Sheet1
    | Column1 | Column2 | Column3 |
    |---------|---------|---------|
    | data1   | data2   | data3   |
    | data4   | data5   | data6   |
    ```

    Key features:
    - Maintains original file structure in headings
    - Processes nested files recursively
    - Uses appropriate converters for each file type
    - Preserves formatting of converted content
    - Cleans up temporary files after processing
    """

    def __init__(
        self,
        *,
        markitdown: "MarkItDown",
    ):
        super().__init__()
        self._markitdown = markitdown

    def accepts(
        self,
        file_stream: BinaryIO,
        stream_info: StreamInfo,
        **kwargs: Any,  # Options to pass to the converter
    ) -> bool:
        mimetype = (stream_info.mimetype or "").lower()
        extension = (stream_info.extension or "").lower()

        if extension in ACCEPTED_FILE_EXTENSIONS:
            return True

        for prefix in ACCEPTED_MIME_TYPE_PREFIXES:
            if mimetype.startswith(prefix):
                return True

        return False

    def convert(
        self,
        file_stream: BinaryIO,
        stream_info: StreamInfo,
        **kwargs: Any,  # Options to pass to the converter
    ) -> DocumentConverterResult:
        file_path = stream_info.url or stream_info.local_path or stream_info.filename
        md_content = f"Content from the zip file `{file_path}`:\n\n"

        with zipfile.ZipFile(file_stream, "r") as zipObj:
            for name in zipObj.namelist():
                try:
                    z_file_stream = io.BytesIO(zipObj.read(name))
                    z_file_stream_info = StreamInfo(
                        extension=os.path.splitext(name)[1],
                        filename=os.path.basename(name),
                    )
                    result = self._markitdown.convert_stream(
                        stream=z_file_stream,
                        stream_info=z_file_stream_info,
                    )
                    if result is not None:
                        md_content += f"## File: {name}\n\n"
                        md_content += result.markdown + "\n\n"
                except UnsupportedFormatException:
                    pass
                except FileConversionException:
                    pass

        return DocumentConverterResult(markdown=md_content.strip())


#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "sqlite-vec.h"
#include "sqlite3.h"
#include <assert.h>

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  if (size < 2) return 0;

  int rc;
  sqlite3 *db;

  rc = sqlite3_open(":memory:", &db);
  assert(rc == SQLITE_OK);
  rc = sqlite3_vec_init(db, NULL, NULL);
  assert(rc == SQLITE_OK);

  // Build a valid table with 3 vectors (float[4] = 16 bytes each)
  // [1,0,0,0], [0,-1,0,1], [1,1,0,1] as little-endian float32 hex
  rc = sqlite3_exec(db,
    "CREATE VIRTUAL TABLE v USING vec0(emb float[4]);"
    "INSERT INTO v(rowid, emb) VALUES (1, X'0000803f000000000000000000000000');"
    "INSERT INTO v(rowid, emb) VALUES (2, X'00000000000080bf000000000000803f');"
    "INSERT INTO v(rowid, emb) VALUES (3, X'0000803f0000803f000000000000803f');",
    NULL, NULL, NULL);
  if (rc != SQLITE_OK) {
    sqlite3_close(db);
    return 0;
  }

  // Use first byte to select corruption strategy
  int target = data[0] % 6;
  const uint8_t *payload = data + 1;
  int payload_size = (int)(size - 1);

  sqlite3_stmt *stmt = NULL;

  switch (target) {
    case 0: {
      // Corrupt _chunks validity blob with fuzz data
      rc = sqlite3_prepare_v2(db,
        "UPDATE v_chunks SET validity = ? WHERE rowid = 1", -1, &stmt, NULL);
      if (rc == SQLITE_OK) {
        sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC);
        sqlite3_step(stmt);
        sqlite3_finalize(stmt);
      }
      break;
    }
    case 1: {
      // Corrupt _chunks rowids blob with fuzz data
      rc = sqlite3_prepare_v2(db,
        "UPDATE v_chunks SET rowids = ? WHERE rowid = 1", -1, &stmt, NULL);
      if (rc == SQLITE_OK) {
        sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC);
        sqlite3_step(stmt);
        sqlite3_finalize(stmt);
      }
      break;
    }
    case 2: {
      // Corrupt _vector_chunks00 vectors blob with fuzz data
      rc = sqlite3_prepare_v2(db,
        "UPDATE v_vector_chunks00 SET vectors = ? WHERE rowid = 1", -1, &stmt, NULL);
      if (rc == SQLITE_OK) {
        sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC);
        sqlite3_step(stmt);
        sqlite3_finalize(stmt);
      }
      break;
    }
    case 3: {
      // Set validity to NULL (violates NOT NULL but shadow tables are writable)
      sqlite3_exec(db,
        "UPDATE v_chunks SET validity = NULL WHERE rowid = 1",
        NULL, NULL, NULL);
      break;
    }
    case 4: {
      // Set rowids to NULL
      sqlite3_exec(db,
        "UPDATE v_chunks SET rowids = NULL WHERE rowid = 1",
        NULL, NULL, NULL);
      break;
    }
    case 5: {
      // Delete shadow table rows entirely (orphan the virtual table data)
      sqlite3_exec(db,
        "DELETE FROM v_vector_chunks00 WHERE rowid = 1",
        NULL, NULL, NULL);
      break;
    }
  }

  // Exercise all read paths — NONE should crash
  sqlite3_exec(db, "SELECT * FROM v", NULL, NULL, NULL);
  sqlite3_exec(db, "SELECT * FROM v WHERE rowid = 1", NULL, NULL, NULL);
  sqlite3_exec(db, "SELECT * FROM v WHERE rowid = 2", NULL, NULL, NULL);
  sqlite3_exec(db,
    "SELECT rowid, distance FROM v "
    "WHERE emb MATCH X'0000803f000000000000000000000000' LIMIT 3",
    NULL, NULL, NULL);
  sqlite3_exec(db, "DELETE FROM v WHERE rowid = 2", NULL, NULL, NULL);
  sqlite3_exec(db,
    "INSERT INTO v(rowid, emb) VALUES (4, X'0000803f000000000000000000000000')",
    NULL, NULL, NULL);
  sqlite3_exec(db, "DROP TABLE v", NULL, NULL, NULL);

  sqlite3_close(db);
  return 0;
}


package io.unitycatalog.server.utils;

import static io.unitycatalog.server.utils.ColumnUtils.addTypeTextAndJsonText;
import static io.unitycatalog.server.utils.ColumnUtils.getTypeJson;
import static io.unitycatalog.server.utils.ColumnUtils.getTypeText;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.unitycatalog.server.model.ColumnTypeName;
import io.unitycatalog.server.model.CreateCatalog;
import io.unitycatalog.server.model.CreateFunction;
import io.unitycatalog.server.model.CreateFunctionRequest;
import io.unitycatalog.server.model.CreateSchema;
import io.unitycatalog.server.model.DataSourceFormat;
import io.unitycatalog.server.model.FunctionParameterInfo;
import io.unitycatalog.server.model.FunctionParameterInfos;
import io.unitycatalog.server.model.FunctionParameterMode;
import io.unitycatalog.server.model.FunctionParameterType;
import io.unitycatalog.server.model.SchemaInfo;
import io.unitycatalog.server.model.TableType;
import io.unitycatalog.server.model.VolumeType;
import io.unitycatalog.server.persist.CatalogRepository;
import io.unitycatalog.server.persist.FunctionRepository;
import io.unitycatalog.server.persist.Repositories;
import io.unitycatalog.server.persist.SchemaRepository;
import io.unitycatalog.server.persist.dao.ColumnInfoDAO;
import io.unitycatalog.server.persist.dao.PropertyDAO;
import io.unitycatalog.server.persist.dao.TableInfoDAO;
import io.unitycatalog.server.persist.dao.VolumeInfoDAO;
import io.unitycatalog.server.persist.utils.HibernateConfigurator;
import io.unitycatalog.server.utils.ServerProperties.Property;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

/**
 * This is a utility class to populate the test database with some sample data. All the quickstart
 * examples in the documentation are based on this sample data. This class is not part of the main
 * application code and is only used for testing purposes. To recreate the sample data, first delete
 * the existing database (at /etc/db) and then run main method of this class by running the command
 * `build/sbt server/populateTestDB`. Any data artifacts which might be referred to by storage
 * location of any table/volume should be created before creating the table/volume entry in the
 * database or can be a part of the same PR.
 */
public class PopulateTestDatabase {

  public static void main(String[] args) throws JsonProcessingException {
    System.out.println("Populating test database...");

    Properties properties = new Properties();
    properties.setProperty(Property.SERVER_ENV.getKey(), "dev");
    ServerProperties serverProperties = new ServerProperties(properties);
    HibernateConfigurator hibernateConfigurator = new HibernateConfigurator(serverProperties);
    Repositories repositories =
        new Repositories(hibernateConfigurator.getSessionFactory(), serverProperties);
    CatalogRepository catalogRepository = repositories.getCatalogRepository();
    SchemaRepository schemaRepository = repositories.getSchemaRepository();

    String catalogName = "unity";
    String schemaName = "default";

    CreateCatalog catalog1 = new CreateCatalog().name(catalogName).comment("Main catalog");
    catalogRepository.addCatalog(catalog1);

    CreateSchema schema1 =
        new CreateSchema().name(schemaName).catalogName(catalogName).comment("Default schema");
    schemaRepository.createSchema(schema1);

    SchemaInfo schemaInfo = schemaRepository.getSchema(catalogName + "." + schemaName);
    String schemaId = schemaInfo.getSchemaId();

    SessionFactory factory = hibernateConfigurator.getSessionFactory();

    // Create managed table
    ColumnInfoDAO idColumn =
        ColumnInfoDAO.builder()
            .name("id")
            .typeName(ColumnTypeName.INT.getValue())
            .comment("ID primary key")
            .ordinalPosition((short) 0)
            .build();
    addTypeTextAndJsonText(idColumn);

    ColumnInfoDAO nameColumn =
        ColumnInfoDAO.builder()
            .name("name")
            .typeName(ColumnTypeName.STRING.getValue())
            .comment("Name of the entity")
            .ordinalPosition((short) 1)
            .build();
    addTypeTextAndJsonText(nameColumn);

    ColumnInfoDAO marksColumn =
        ColumnInfoDAO.builder()
            .name("marks")
            .typeName(ColumnTypeName.INT.getValue())
            .comment("Marks of the entity")
            .ordinalPosition((short) 2)
            .nullable(true)
            .build();
    addTypeTextAndJsonText(marksColumn);

    UUID tableId = UUID.randomUUID();

    String tableName = "marksheet";
    String storageRoot = "etc/data/managed/";
    String tablePath = storageRoot + catalogName + "/" + schemaName + "/tables/" + tableName;

    System.setProperty("storageRoot", storageRoot);

    TableInfoDAO tableInfoDAO =
        TableInfoDAO.builder()
            .id(tableId)
            .name(tableName)
            .schemaId(UUID.fromString(schemaId))
            .comment("Managed table")
            .columns(List.of(idColumn, nameColumn, marksColumn))
            .dataSourceFormat(DataSourceFormat.DELTA.getValue())
            .type(TableType.MANAGED.getValue())
            .createdAt(new Date())
            .updatedAt(new Date())
            .url(tablePath)
            .build();

    tableInfoDAO.getColumns().forEach(columnInfoDAO -> columnInfoDAO.setTable(tableInfoDAO));

    PropertyDAO p1 =
        PropertyDAO.builder()
            .key("key1")
            .value("value1")
            .entityId(tableId)
            .entityType("table")
            .build();
    PropertyDAO p2 =
        PropertyDAO.builder()
            .key("key2")
            .value("value2")
            .entityId(tableId)
            .entityType("table")
            .build();

    try (Session session = factory.openSession()) {
      Transaction tx = session.beginTransaction();
      session.persist(tableInfoDAO);
      session.persist(p1);
      session.persist(p2);
      tx.commit();
    }

    // create uniform table
    String uniformTableName = "marksheet_uniform";
    String uniformTablePath = "file:///tmp/" + uniformTableName;
    UUID uniformTableId = UUID.randomUUID();
    ColumnInfoDAO idColumnUniform =
        ColumnInfoDAO.builder()
            .name("id")
            .typeName(ColumnTypeName.INT.getValue())
            .comment("ID primary key")
            .ordinalPosition((short) 0)
            .build();
    addTypeTextAndJsonText(idColumnUniform);

    ColumnInfoDAO nameColumnUniform =
        ColumnInfoDAO.builder()
            .name("name")
            .typeName(ColumnTypeName.STRING.getValue())
            .comment("Name of the entity")
            .ordinalPosition((short) 1)
            .build();
    addTypeTextAndJsonText(nameColumnUniform);

    ColumnInfoDAO marksColumnUniform =
        ColumnInfoDAO.builder()
            .name("marks")
            .typeName(ColumnTypeName.INT.getValue())
            .comment("Marks of the entity")
            .ordinalPosition((short) 2)
            .nullable(true)
            .build();
    addTypeTextAndJsonText(marksColumnUniform);

    TableInfoDAO uniformTableInfoDAO =
        TableInfoDAO.builder()
            .id(uniformTableId)
            .name(tableName + "_uniform")
            .schemaId(UUID.fromString(schemaId))
            .comment("Uniform table")
            .columns(List.of(idColumnUniform, nameColumnUniform, marksColumnUniform))
            .dataSourceFormat(DataSourceFormat.DELTA.getValue())
            .type(TableType.EXTERNAL.getValue())
            .createdAt(new Date())
            .updatedAt(new Date())
            .url(uniformTablePath)
            .uniformIcebergMetadataLocation(
                uniformTablePath
                    + "/metadata/00002-5b7aa739-d074-4764-b49d-ad6c63419576.metadata.json")
            .build();
    uniformTableInfoDAO
        .getColumns()
        .forEach(columnInfoDAO -> columnInfoDAO.setTable(uniformTableInfoDAO));
    PropertyDAO p1uniform =
        PropertyDAO.builder()
            .key("key1")
            .value("value1")
            .entityId(uniformTableId)
            .entityType("table")
            .build();
    PropertyDAO p2uniform =
        PropertyDAO.builder()
            .key("key2")
            .value("value2")
            .entityId(uniformTableId)
            .entityType("table")
            .build();
    try (Session session = factory.openSession()) {
      Transaction tx = session.beginTransaction();
      session.persist(uniformTableInfoDAO);
      session.persist(p1uniform);
      session.persist(p2uniform);
      tx.commit();
    }

    // create external table
    ColumnInfoDAO idColumn1 =
        ColumnInfoDAO.builder()
            .name("as_int")
            .typeName(ColumnTypeName.INT.getValue())
            .comment("Int column")
            .ordinalPosition((short) 0)
            .build();
    addTypeTextAndJsonText(idColumn1);
    ColumnInfoDAO doubleColumn2 =
        ColumnInfoDAO.builder()
            .name("as_double")
            .typeName(ColumnTypeName.DOUBLE.getValue())
            .comment("Double column")
            .ordinalPosition((short) 1)
            .build();
    addTypeTextAndJsonText(doubleColumn2);

    String externalTableName = "numbers";
    String externalStorageRoot = "etc/data/external/";
    String externalTablePath =
        externalStorageRoot + catalogName + "/" + schemaName + "/tables/" + externalTableName;

    UUID externalTableId = UUID.randomUUID();

    System.setProperty("storageRoot", externalStorageRoot);

    TableInfoDAO externalTableInfoDAO =
        TableInfoDAO.builder()
            .id(externalTableId)
            .name(externalTableName)
            .schemaId(UUID.fromString(schemaId))
            .comment("External table")
            .columns(List.of(idColumn1, doubleColumn2))
            .dataSourceFormat(DataSourceFormat.DELTA.getValue())
            .type(TableType.EXTERNAL.getValue())
            .createdAt(new Date())
            .updatedAt(new Date())
            .url(externalTablePath)
            .build();

    externalTableInfoDAO
        .getColumns()
        .forEach(columnInfoDAO -> columnInfoDAO.setTable(externalTableInfoDAO));

    PropertyDAO p11 =
        PropertyDAO.builder()
            .key("key1")
            .value("value1")
            .entityId(externalTableId)
            .entityType("table")
            .build();
    PropertyDAO p21 =
        PropertyDAO.builder()
            .key("key2")
            .value("value2")
            .entityId(externalTableId)
            .entityType("table")
            .build();

    try (Session session = factory.openSession()) {
      Transaction tx = session.beginTransaction();
      session.persist(externalTableInfoDAO);
      session.persist(p11);
      session.persist(p21);
      tx.commit();
    }

    // Create external partitioned table.
    // This table represents an example of how Unity handles partitioned tables.
    // The table contains three columns:
    // - first_name
    // - age
    // - country (partition column)
    // All the data in this table are fake and were generated by tool Faker.
    // Partition column has three unique values / partitions.
    // Data is stored in DELTA format.
    System.out.println("Create external partitioned table...");

    // Add columns
    ColumnInfoDAO firstName =
        ColumnInfoDAO.builder()
            .name("first_name")
            .typeName(ColumnTypeName.STRING.getValue())
            .comment("string column")
            .ordinalPosition((short) 0)
            .build();
    addTypeTextAndJsonText(firstName);

    ColumnInfoDAO age =
        ColumnInfoDAO.builder()
            .name("age")
            .typeName(ColumnTypeName.LONG.getValue())
            .comment("long column")
            .ordinalPosition((short) 1)
            .build();
    addTypeTextAndJsonText(age);

    ColumnInfoDAO country =
        ColumnInfoDAO.builder()
            .name("country")
            .typeName(ColumnTypeName.STRING.getValue())
            .comment("partition column")
            .ordinalPosition((short) 2)
            .partitionIndex((short) 0)
            .build();
    addTypeTextAndJsonText(country);

    // Create table
    String partitionedTableName = "user_countries";
    String partitionedStorageRoot = "etc/data/external/";
    String partitionedTablePath =
        partitionedStorageRoot + catalogName + "/" + schemaName + "/tables/" + partitionedTableName;
    UUID partitionedTableId = UUID.randomUUID();

    TableInfoDAO partitionedTableInfoDAO =
        TableInfoDAO.builder()
            .id(partitionedTableId)
            .name(partitionedTableName)
            .schemaId(UUID.fromString(schemaId))
            .comment("Partitioned table")
            .columns(List.of(firstName, age, country))
            .dataSourceFormat(DataSourceFormat.DELTA.getValue())
            .type(TableType.EXTERNAL.getValue())
            .createdAt(new Date())
            .updatedAt(new Date())
            .url(partitionedTablePath)
            .build();

    partitionedTableInfoDAO
        .getColumns()
        .forEach(columnInfoDAO -> columnInfoDAO.setTable(partitionedTableInfoDAO));

    try (Session session = factory.openSession()) {
      Transaction trx = session.beginTransaction();
      session.persist(partitionedTableInfoDAO);
      trx.commit();
    }

    System.out.println("Creating managed/external Volume...");
    VolumeInfoDAO managedVolume =
        VolumeInfoDAO.builder()
            .volumeType(VolumeType.MANAGED.getValue())
            .storageLocation("etc/data/managed/unity/default/volumes/txt_files")
            .name("txt_files")
            .createdAt(new Date())
            .updatedAt(new Date())
            .id(UUID.randomUUID())
            .schemaId(UUID.fromString(schemaId))
            .build();

    VolumeInfoDAO externalVolume =
        VolumeInfoDAO.builder()
            .volumeType(VolumeType.EXTERNAL.getValue())
            .storageLocation("etc/data/external/unity/default/volumes/json_files")
            .name("json_files")
            .createdAt(new Date())
            .updatedAt(new Date())
            .id(UUID.randomUUID())
            .schemaId(UUID.fromString(schemaId))
            .build();

    try (Session session = factory.openSession()) {
      session.beginTransaction();
      session.persist(managedVolume);
      session.persist(externalVolume);
      session.getTransaction().commit();
    }

    insertFunctionSampleData(catalogName, schemaName, repositories);
  }

  public static void insertFunctionSampleData(
      String catalog, String schema, Repositories repositories) {

    // Create function objects
    CreateFunction sumFunction = new CreateFunction();
    sumFunction.setName("sum");
    sumFunction.setCatalogName(catalog);
    sumFunction.setSchemaName(schema);
    sumFunction.setComment("Adds two numbers.");
    sumFunction.setDataType(ColumnTypeName.INT);
    sumFunction.setFullDataType(ColumnTypeName.INT.getValue());
    sumFunction.setExternalLanguage("python");
    sumFunction.setIsDeterministic(true);
    sumFunction.setIsNullCall(false);
    sumFunction.setParameterStyle(CreateFunction.ParameterStyleEnum.S);
    sumFunction.setRoutineBody(CreateFunction.RoutineBodyEnum.EXTERNAL);
    sumFunction.setRoutineDefinition("t = x + y + z\\nreturn t");
    sumFunction.setSqlDataAccess(CreateFunction.SqlDataAccessEnum.NO_SQL);
    sumFunction.setSecurityType(CreateFunction.SecurityTypeEnum.DEFINER);
    sumFunction.setSpecificName("sum");

    CreateFunction stringLowercaseFunction = new CreateFunction();
    stringLowercaseFunction.setName("lowercase");
    stringLowercaseFunction.setCatalogName(catalog);
    stringLowercaseFunction.setSchemaName(schema);
    stringLowercaseFunction.setComment("Converts a string to lowercase.");
    stringLowercaseFunction.setDataType(ColumnTypeName.STRING);
    stringLowercaseFunction.setFullDataType(ColumnTypeName.STRING.getValue());
    stringLowercaseFunction.setExternalLanguage("python");
    stringLowercaseFunction.setIsDeterministic(true);
    stringLowercaseFunction.setIsNullCall(false);
    stringLowercaseFunction.setParameterStyle(CreateFunction.ParameterStyleEnum.S);
    stringLowercaseFunction.setRoutineBody(CreateFunction.RoutineBodyEnum.EXTERNAL);
    stringLowercaseFunction.setRoutineDefinition("g = s.lower()\\nreturn g");
    stringLowercaseFunction.setSqlDataAccess(CreateFunction.SqlDataAccessEnum.NO_SQL);
    stringLowercaseFunction.setSecurityType(CreateFunction.SecurityTypeEnum.DEFINER);
    stringLowercaseFunction.setSpecificName("lowercase");

    // Create parameter objects for sum function

    FunctionParameterInfo sumParam1 = new FunctionParameterInfo();
    sumParam1.setName("x");
    sumParam1.setTypeText(getTypeText(ColumnTypeName.INT));
    sumParam1.setTypeJson(getTypeJson(ColumnTypeName.INT, "x", false, null));
    sumParam1.setTypeName(ColumnTypeName.INT);
    sumParam1.setPosition(0);
    sumParam1.setParameterMode(FunctionParameterMode.IN);
    sumParam1.setParameterType(FunctionParameterType.PARAM);

    FunctionParameterInfo sumParam2 = new FunctionParameterInfo();
    sumParam2.setName("y");
    sumParam2.setTypeText(getTypeText(ColumnTypeName.INT));
    sumParam2.setTypeJson(getTypeJson(ColumnTypeName.INT, "y", false, null));
    sumParam2.setTypeName(ColumnTypeName.INT);
    sumParam2.setPosition(1);
    sumParam2.setParameterMode(FunctionParameterMode.IN);
    sumParam2.setParameterType(FunctionParameterType.PARAM);

    FunctionParameterInfo sumParam3 = new FunctionParameterInfo();
    sumParam3.setName("z");
    sumParam3.setTypeText(getTypeText(ColumnTypeName.INT));
    sumParam3.setTypeJson(getTypeJson(ColumnTypeName.INT, "z", false, null));
    sumParam3.setTypeName(ColumnTypeName.INT);
    sumParam3.setPosition(2);
    sumParam3.setParameterMode(FunctionParameterMode.IN);
    sumParam3.setParameterType(FunctionParameterType.PARAM);

    // Create parameter objects for lowercase function
    FunctionParameterInfo lowercaseParam = new FunctionParameterInfo();
    lowercaseParam.setName("s");
    lowercaseParam.setTypeText(getTypeText(ColumnTypeName.STRING));
    lowercaseParam.setTypeJson(getTypeJson(ColumnTypeName.STRING, "s", false, null));
    lowercaseParam.setTypeName(ColumnTypeName.STRING);
    lowercaseParam.setPosition(0);
    lowercaseParam.setParameterMode(FunctionParameterMode.IN);
    lowercaseParam.setParameterType(FunctionParameterType.PARAM);

    FunctionParameterInfos functionParameterInfos = new FunctionParameterInfos();
    functionParameterInfos.setParameters(List.of(sumParam1, sumParam2, sumParam3));
    sumFunction.setInputParams(functionParameterInfos);

    FunctionParameterInfos stringLowercaseFunctionParameterInfos = new FunctionParameterInfos();
    stringLowercaseFunctionParameterInfos.setParameters(List.of(lowercaseParam));
    stringLowercaseFunction.setInputParams(stringLowercaseFunctionParameterInfos);

    FunctionRepository functionRepository = repositories.getFunctionRepository();
    functionRepository.createFunction(new CreateFunctionRequest().functionInfo(sumFunction));

    functionRepository.createFunction(
        new CreateFunctionRequest().functionInfo(stringLowercaseFunction));
  }
}


"""Tests for `LinkExtractor`."""

import re

import pytest

from scrapling.engines.toolbelt.custom import Response
from scrapling.spiders.links import IGNORED_EXTENSIONS, LinkExtractor

def _make_response(html: str, url: str = "https://example.com/page") -> Response:
    """Build a minimal Response wrapping the given HTML."""
    return Response(
        url=url,
        content=html,
        status=200,
        reason="OK",
        cookies={},
        headers={},
        request_headers={},
    )

HTML_BASIC = """

  post 1
  post 2
  external
  about
  mail
  js
  pdf
  <area href="/area-link">area</area>
  <link rel="stylesheet" href="/style.css">

"""

class TestExtractBasic:
    def test_default_extracts_a_and_area_with_href(self):
        resp = _make_response(HTML_BASIC)
        urls = LinkExtractor().extract(resp)
        # mailto/javascript filtered (non-http scheme), .pdf filtered (deny_extensions)
        # link[rel=stylesheet] not in default tags
        assert "https://example.com/posts/1" in urls
        assert "https://example.com/posts/2" in urls
        assert "https://example.com/about" in urls
        assert "https://example.com/area-link" in urls
        assert "https://other.com/page" in urls
        assert all(not u.startswith("mailto:") for u in urls)
        assert all(not u.startswith("javascript:") for u in urls)
        assert not any(u.endswith(".pdf") for u in urls)
        assert not any(u.endswith(".css") for u in urls)

    def test_relative_urls_become_absolute_via_urljoin(self):
        resp = _make_response('x', url="https://example.com/sub/")
        assert LinkExtractor().extract(resp) == ["https://example.com/sub/foo/bar"]

    def test_empty_allow_means_match_all(self):
        resp = _make_response('xy')
        out = LinkExtractor().extract(resp)
        assert len(out) == 2

class TestAllowDeny:
    def test_allow_regex_filters_in(self):
        resp = _make_response(HTML_BASIC)
        urls = LinkExtractor(allow=r"/posts/").extract(resp)
        assert urls == ["https://example.com/posts/1", "https://example.com/posts/2"]

    def test_deny_regex_filters_out(self):
        resp = _make_response(HTML_BASIC)
        urls = LinkExtractor(deny=r"/posts/").extract(resp)
        assert "https://example.com/posts/1" not in urls
        assert "https://example.com/about" in urls

    def test_deny_overrides_allow(self):
        resp = _make_response(HTML_BASIC)
        urls = LinkExtractor(allow=r"/posts/", deny=r"/posts/2").extract(resp)
        assert urls == ["https://example.com/posts/1"]

    def test_compiled_pattern_accepted(self):
        resp = _make_response(HTML_BASIC)
        pat = re.compile(r"/posts/\d+$")
        urls = LinkExtractor(allow=pat).extract(resp)
        assert len(urls) == 2

    def test_iterable_of_patterns(self):
        resp = _make_response(HTML_BASIC)
        urls = LinkExtractor(allow=[r"/posts/", r"/about"]).extract(resp)
        assert "https://example.com/posts/1" in urls
        assert "https://example.com/about" in urls

class TestDomains:
    def test_allow_domains_keeps_only_matching(self):
        resp = _make_response(HTML_BASIC)
        urls = LinkExtractor(allow_domains="example.com").extract(resp)
        assert all("example.com" in u for u in urls)
        assert "https://other.com/page" not in urls

    def test_allow_domains_matches_subdomains(self):
        html = 'ab'
        resp = _make_response(html)
        urls = LinkExtractor(allow_domains="example.com").extract(resp)
        assert urls == ["https://api.example.com/x"]

    def test_deny_domains_filters_out(self):
        resp = _make_response(HTML_BASIC)
        urls = LinkExtractor(deny_domains="other.com").extract(resp)
        assert "https://other.com/page" not in urls
        assert "https://example.com/posts/1" in urls

class TestRestrict:
    def test_restrict_css_scopes_extraction(self):
        html = """

            n
            m

        """
        resp = _make_response(html)
        urls = LinkExtractor(restrict_css="main").extract(resp)
        assert urls == ["https://example.com/main-link"]

    def test_restrict_xpath_scopes_extraction(self):
        html = """

h

c

        """
        resp = _make_response(html)
        urls = LinkExtractor(restrict_xpath='//div[@id="content"]').extract(resp)
        assert urls == ["https://example.com/c"]

class TestTagsAttrs:
    def test_custom_tags_and_attrs_for_stylesheets(self):
        html = '<link rel="stylesheet" href="/style.css">p'
        resp = _make_response(html)
        # Override deny_extensions to allow .css through, and pick up <link href>
        urls = LinkExtractor(tags=("link",), attrs=("href",), deny_extensions=()).extract(resp)
        assert urls == ["https://example.com/style.css"]

class TestCanonicalization:
    def test_query_params_sorted(self):
        resp = _make_response('x')
        urls = LinkExtractor().extract(resp)
        assert urls == ["https://example.com/x?a=1&b=2"]

    def test_fragment_dropped_by_default(self):
        resp = _make_response('x')
        urls = LinkExtractor().extract(resp)
        assert urls == ["https://example.com/x"]

    def test_keep_fragment_preserves_it(self):
        resp = _make_response('x')
        urls = LinkExtractor(keep_fragment=True).extract(resp)
        assert urls == ["https://example.com/x#section"]

    def test_canonicalize_off_leaves_url_unchanged(self):
        resp = _make_response('x')
        urls = LinkExtractor(canonicalize=False).extract(resp)
        assert urls == ["https://example.com/x?b=2&a=1#f"]

class TestDedup:
    def test_unique_drops_duplicates(self):
        html = 'abc'
        resp = _make_response(html)
        urls = LinkExtractor().extract(resp)
        # canonicalize collapses /x and /x? together
        assert urls == ["https://example.com/x"]

class TestExtensions:
    def test_default_deny_extensions_drops_pdf_zip_images(self):
        html = 'pdfzippngok'
        resp = _make_response(html)
        urls = LinkExtractor().extract(resp)
        assert urls == ["https://example.com/d"]

    def test_default_deny_extensions_drops_compound_archive(self):
        html = 'archiveok'
        resp = _make_response(html)
        urls = LinkExtractor().extract(resp)
        assert urls == ["https://example.com/d"]

    def test_custom_deny_extensions_overrides_default(self):
        html = 'pdfzip'
        resp = _make_response(html)
        urls = LinkExtractor(deny_extensions={"zip"}).extract(resp)
        # .pdf now allowed because we replaced the set
        assert urls == ["https://example.com/a.pdf"]

    def test_custom_deny_extensions_honors_compound_extension(self):
        ex = LinkExtractor(deny_extensions={"tar.gz"})
        assert ex.matches("https://example.com/dataset.tar.gz") is False
        assert ex.matches("https://example.com/dataset.gz") is True

    def test_empty_deny_extensions_allows_everything(self):
        html = 'pdf'
        resp = _make_response(html)
        urls = LinkExtractor(deny_extensions=()).extract(resp)
        assert urls == ["https://example.com/a.pdf"]

class TestStrip:
    def test_strip_removes_whitespace(self):
        resp = _make_response('x')
        urls = LinkExtractor().extract(resp)
        assert urls == ["https://example.com/spaced"]

class TestMatches:
    def test_matches_honors_allow(self):
        ex = LinkExtractor(allow=r"/posts/")
        assert ex.matches("https://example.com/posts/1") is True
        assert ex.matches("https://example.com/about") is False

    def test_matches_honors_deny(self):
        ex = LinkExtractor(deny=r"/admin")
        assert ex.matches("https://example.com/admin/x") is False
        assert ex.matches("https://example.com/posts/1") is True

    def test_matches_honors_allow_domains(self):
        ex = LinkExtractor(allow_domains="example.com")
        assert ex.matches("https://api.example.com/x") is True
        assert ex.matches("https://other.com/x") is False

    def test_matches_honors_deny_extensions(self):
        ex = LinkExtractor()
        assert ex.matches("https://example.com/file.pdf") is False
        assert ex.matches("https://example.com/page") is True

    def test_matches_rejects_non_http_schemes(self):
        ex = LinkExtractor()
        assert ex.matches("mailto:x@example.com") is False
        assert ex.matches("javascript:void(0)") is False
        assert ex.matches("ftp://example.com/x") is False

    def test_matches_canonicalizes_before_checking(self):
        ex = LinkExtractor(allow=r"a=1&b=2$")
        # the URL has params in the wrong order; canonicalize sorts them
        assert ex.matches("https://example.com/x?b=2&a=1") is True

class TestIgnoredExtensions:
    def test_constant_includes_common_binary_types(self):
        for ext in ("pdf", "zip", "png", "mp4", "exe"):
            assert ext in IGNORED_EXTENSIONS


package com.netflix.maestro.signal.messageprocessors;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.netflix.maestro.AssertHelper;
import com.netflix.maestro.engine.metrics.MaestroMetricRepo;
import com.netflix.maestro.models.signal.SignalInstance;
import com.netflix.maestro.signal.dao.MaestroSignalBrokerDao;
import com.netflix.maestro.signal.metrics.MetricConstants;
import com.netflix.maestro.signal.models.SignalTriggerMatch;
import com.netflix.maestro.signal.producer.SignalQueueProducer;
import com.netflix.spectator.api.DefaultRegistry;
import java.util.List;
import java.util.function.Supplier;
import org.junit.Before;
import org.junit.Test;

/**
 * Tests for SignalInstanceProcessor class.
 *
 * @author jun-he
 */
public class SignalInstanceProcessorTest {
  private MaestroSignalBrokerDao brokerDao;
  private SignalQueueProducer producer;
  private MaestroMetricRepo metricRepo;
  private SignalInstanceProcessor processor;

  @Before
  public void setup() {
    brokerDao = mock(MaestroSignalBrokerDao.class);
    producer = mock(SignalQueueProducer.class);
    metricRepo = new MaestroMetricRepo(new DefaultRegistry());
    processor = new SignalInstanceProcessor(brokerDao, producer, metricRepo);
  }

  @Test
  public void testProcessWithMatch() {
    SignalInstance instance = new SignalInstance();
    instance.setSeqId(12);
    Supplier<SignalInstance> messageSupplier = () -> instance;
    when(brokerDao.getSubscribedTriggers(any())).thenReturn(List.of(new SignalTriggerMatch()));
    processor.process(messageSupplier);
    verify(producer, times(1)).push(any(SignalTriggerMatch.class));
    assertEquals(
        1L,
        metricRepo
            .getCounter(MetricConstants.SIGNAL_TRIGGER_MATCH_FOUND, SignalInstanceProcessor.class)
            .count());
  }

  @Test
  public void testProcessWithoutMatch() {
    SignalInstance instance = new SignalInstance();
    instance.setSeqId(12);
    Supplier<SignalInstance> messageSupplier = () -> instance;
    when(brokerDao.getSubscribedTriggers(any())).thenReturn(List.of());
    processor.process(messageSupplier);
    verify(producer, times(0)).push(any(SignalTriggerMatch.class));
    assertEquals(
        0L,
        metricRepo
            .getCounter(MetricConstants.SIGNAL_TRIGGER_MATCH_FOUND, SignalInstanceProcessor.class)
            .count());
  }

  @Test
  public void testProcessInvalidSignalInstance() {
    Supplier<SignalInstance> messageSupplier = SignalInstance::new;
    AssertHelper.assertThrows(
        "Invalid seq id",
        IllegalArgumentException.class,
        "it must be positive",
        () -> processor.process(messageSupplier));
  }
}


package io.unitycatalog.server.base.function;

import static io.unitycatalog.server.utils.TestUtils.CATALOG_NAME;
import static io.unitycatalog.server.utils.TestUtils.CATALOG_NEW_NAME;
import static io.unitycatalog.server.utils.TestUtils.COMMON_ENTITY_NAME;
import static io.unitycatalog.server.utils.TestUtils.FUNCTION_FULL_NAME;
import static io.unitycatalog.server.utils.TestUtils.FUNCTION_NAME;
import static io.unitycatalog.server.utils.TestUtils.SCHEMA_NAME;
import static org.apache.iceberg.view.ViewProperties.COMMENT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import io.unitycatalog.client.ApiException;
import io.unitycatalog.client.model.ColumnTypeName;
import io.unitycatalog.client.model.CreateCatalog;
import io.unitycatalog.client.model.CreateFunction;
import io.unitycatalog.client.model.CreateFunctionRequest;
import io.unitycatalog.client.model.CreateSchema;
import io.unitycatalog.client.model.FunctionInfo;
import io.unitycatalog.client.model.FunctionParameterInfo;
import io.unitycatalog.client.model.FunctionParameterInfos;
import io.unitycatalog.client.model.UpdateCatalog;
import io.unitycatalog.server.base.BaseCRUDTest;
import io.unitycatalog.server.base.ServerConfig;
import io.unitycatalog.server.base.schema.SchemaOperations;
import java.util.List;
import java.util.Optional;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public abstract class BaseFunctionCRUDTest extends BaseCRUDTest {
  protected SchemaOperations schemaOperations;
  protected FunctionOperations functionOperations;

  protected abstract SchemaOperations createSchemaOperations(ServerConfig serverConfig);

  protected abstract FunctionOperations createFunctionOperations(ServerConfig serverConfig);

  @BeforeEach
  @Override
  public void setUp() {
    super.setUp();
    schemaOperations = createSchemaOperations(serverConfig);
    functionOperations = createFunctionOperations(serverConfig);
  }

  protected void createCommonResources() throws ApiException {
    CreateCatalog createCatalog = new CreateCatalog().name(CATALOG_NAME).comment(COMMENT);
    catalogOperations.createCatalog(createCatalog);
    schemaOperations.createSchema(new CreateSchema().name(SCHEMA_NAME).catalogName(CATALOG_NAME));
  }

  protected void assertFunction(FunctionInfo functionInfo, String functionName) {
    assertThat(functionInfo.getName()).isEqualTo(functionName);
    assertThat(functionInfo.getCatalogName()).isEqualTo(CATALOG_NAME);
    assertThat(functionInfo.getSchemaName()).isEqualTo(SCHEMA_NAME);
    assertThat(functionInfo.getFunctionId()).isNotNull();
  }

  @Test
  public void testFunctionCRUD() throws ApiException {
    assertThatThrownBy(() -> functionOperations.getFunction(FUNCTION_FULL_NAME))
        .isInstanceOf(Exception.class);
    // Create a catalog
    createCommonResources();

    // Create a function
    System.out.println("Testing create function..");
    FunctionParameterInfos functionParameterInfos =
        new FunctionParameterInfos()
            .parameters(
                List.of(
                    new FunctionParameterInfo()
                        .name("param1")
                        .typeName(ColumnTypeName.INT)
                        .typeText("int")
                        .typeJson("{\"type\":\"int\"}")
                        .position(0)));
    CreateFunction createFunction =
        new CreateFunction()
            .name(FUNCTION_NAME)
            .catalogName(CATALOG_NAME)
            .schemaName(SCHEMA_NAME)
            .parameterStyle(CreateFunction.ParameterStyleEnum.S)
            .isDeterministic(true)
            .comment(COMMENT)
            .externalLanguage("python")
            .dataType(ColumnTypeName.INT)
            .fullDataType("Integer")
            .isNullCall(false)
            .routineBody(CreateFunction.RoutineBodyEnum.EXTERNAL)
            .routineDefinition("def test():\n  return 1")
            .securityType(CreateFunction.SecurityTypeEnum.DEFINER)
            .specificName("test")
            .sqlDataAccess(CreateFunction.SqlDataAccessEnum.NO_SQL)
            .inputParams(functionParameterInfos);
    CreateFunctionRequest createFunctionRequest =
        new CreateFunctionRequest().functionInfo(createFunction);
    FunctionInfo functionInfo = functionOperations.createFunction(createFunctionRequest);
    assertFunction(functionInfo, FUNCTION_NAME);

    // Create another function to test pagination
    FunctionParameterInfos functionParameterInfos2 =
        new FunctionParameterInfos()
            .parameters(
                List.of(
                    new FunctionParameterInfo()
                        .name("param2")
                        .typeName(ColumnTypeName.INT)
                        .typeText("int")
                        .typeJson("{\"type\":\"int\"}")
                        .position(0)));
    CreateFunction createFunction2 =
        new CreateFunction()
            .name(COMMON_ENTITY_NAME)
            .catalogName(CATALOG_NAME)
            .schemaName(SCHEMA_NAME)
            .parameterStyle(CreateFunction.ParameterStyleEnum.S)
            .isDeterministic(true)
            .comment(COMMENT)
            .externalLanguage("python")
            .dataType(ColumnTypeName.INT)
            .fullDataType("Integer")
            .isNullCall(false)
            .routineBody(CreateFunction.RoutineBodyEnum.EXTERNAL)
            .routineDefinition("def test():\n  return 1")
            .securityType(CreateFunction.SecurityTypeEnum.DEFINER)
            .specificName("test")
            .sqlDataAccess(CreateFunction.SqlDataAccessEnum.NO_SQL)
            .inputParams(functionParameterInfos2);
    CreateFunctionRequest createFunctionRequest2 =
        new CreateFunctionRequest().functionInfo(createFunction2);
    FunctionInfo functionInfo2 = functionOperations.createFunction(createFunctionRequest2);
    assertFunction(functionInfo2, COMMON_ENTITY_NAME);

    // List functions
    System.out.println("Testing list functions..");
    Iterable<FunctionInfo> functionInfos =
        functionOperations.listFunctions(CATALOG_NAME, SCHEMA_NAME, Optional.empty());
    assertThat(functionInfos)
        .as(
            "Function with ID '%s' and parameter '%s' does not exist",
            functionInfo.getFunctionId(), "param1")
        .anySatisfy(
            f -> {
              assertThat(f.getFunctionId()).isNotNull().isEqualTo(functionInfo.getFunctionId());
              assertThat(f.getInputParams())
                  .isNotNull()
                  .extracting(
                      FunctionParameterInfos::getParameters,
                      Assertions.as(InstanceOfAssertFactories.list(FunctionParameterInfo.class)))
                  .isNotNull()
                  .anySatisfy(parameter -> assertThat(parameter.getName()).isEqualTo("param1"));
            });

    // List functions with page token
    System.out.println("Testing list functions with page token..");
    functionInfos =
        functionOperations.listFunctions(CATALOG_NAME, SCHEMA_NAME, Optional.of(FUNCTION_NAME));
    assertThat(functionInfos)
        .as(
            "Function with ID '%s' and parameter '%s' does not exist",
            functionInfo2.getFunctionId(), "param2")
        .noneSatisfy(f -> assertThat(f.getFunctionId()).isEqualTo(functionInfo.getFunctionId()))
        .anySatisfy(
            f -> {
              assertThat(f.getFunctionId()).isNotNull().isEqualTo(functionInfo2.getFunctionId());
              assertThat(f.getInputParams())
                  .isNotNull()
                  .extracting(
                      FunctionParameterInfos::getParameters,
                      Assertions.as(InstanceOfAssertFactories.list(FunctionParameterInfo.class)))
                  .isNotNull()
                  .anySatisfy(parameter -> assertThat(parameter.getName()).isEqualTo("param2"));
            });

    // Get function
    System.out.println("Testing get function..");
    FunctionInfo retrievedFunctionInfo = functionOperations.getFunction(FUNCTION_FULL_NAME);
    assertThat(retrievedFunctionInfo).isEqualTo(functionInfo);

    // now update the parent catalog
    UpdateCatalog updateCatalog = new UpdateCatalog().newName(CATALOG_NEW_NAME);
    catalogOperations.updateCatalog(CATALOG_NAME, updateCatalog);
    // get the function again
    FunctionInfo retrievedFunctionInfoAfterCatUpdate =
        functionOperations.getFunction(CATALOG_NEW_NAME + "." + SCHEMA_NAME + "." + FUNCTION_NAME);
    assertThat(retrievedFunctionInfoAfterCatUpdate.getFunctionId())
        .isEqualTo(retrievedFunctionInfo.getFunctionId());

    // Delete function
    System.out.println("Testing delete function..");
    functionOperations.deleteFunction(
        CATALOG_NEW_NAME + "." + SCHEMA_NAME + "." + FUNCTION_NAME, true);
    assertThat(functionOperations.listFunctions(CATALOG_NEW_NAME, SCHEMA_NAME, Optional.empty()))
        .as("Function with ID '%s' exists", functionInfo.getFunctionId())
        .noneSatisfy(f -> assertThat(f.getFunctionId()).isEqualTo(functionInfo.getFunctionId()));
  }
}


/*
 * Copyright 2024 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.netflix.maestro.models.parameter;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.netflix.maestro.models.definition.TagList;
import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

/** BOOLEAN Parameter definition. */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder(
    value = {"name", "value", "expression", "type", "validator", "tags", "mode"},
    alphabetic = true)
@JsonDeserialize(builder = BooleanParamDefinition.BooleanParamDefinitionBuilderImpl.class)
@Getter(onMethod = @__({@Override}))
@SuperBuilder(toBuilder = true)
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public final class BooleanParamDefinition extends AbstractParamDefinition {
  private final Boolean value;

  @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
  @JsonPOJOBuilder(withPrefix = "")
  static final class BooleanParamDefinitionBuilderImpl
      extends BooleanParamDefinitionBuilder<
          BooleanParamDefinition, BooleanParamDefinitionBuilderImpl> {
    @Override
    public BooleanParamDefinition build() {
      BooleanParamDefinition param = new BooleanParamDefinition(this);
      param.validate();
      return param;
    }
  }

  @Override
  public ParamType getType() {
    return ParamType.BOOLEAN;
  }

  @Override
  public BooleanParamDefinition asBooleanParamDef() {
    return this;
  }

  @Override
  public Parameter toParameter() {
    return BooleanParameter.builder()
        .name(getName())
        .value(getValue())
        .expression(getExpression())
        .validator(getValidator())
        .tags(getTags())
        .mode(getMode())
        .meta(getMeta())
        .build();
  }

  @Override
  public ParamDefinition copyAndUpdate(
      Object updatedValue,
      String expression,
      ParamMode mode,
      Map<String, Object> meta,
      TagList tagList,
      ParamValidator validator) {
    return toBuilder()
        .value((Boolean) updatedValue)
        .expression(expression)
        .validator(validator)
        .tags(tagList)
        .mode(mode)
        .meta(meta)
        .build();
  }
}


import json
from typing import overload

from langchain_core.messages import (  # pyright: ignore
	AIMessage,
	HumanMessage,
	SystemMessage,
)
from langchain_core.messages import (  # pyright: ignore
	ToolCall as LangChainToolCall,
)
from langchain_core.messages.base import BaseMessage as LangChainBaseMessage  # pyright: ignore

from browser_use.llm.messages import (
	AssistantMessage,
	BaseMessage,
	ContentPartImageParam,
	ContentPartRefusalParam,
	ContentPartTextParam,
	ToolCall,
	UserMessage,
)
from browser_use.llm.messages import (
	SystemMessage as BrowserUseSystemMessage,
)

class LangChainMessageSerializer:
	"""Serializer for converting between browser-use message types and LangChain message types."""

	@staticmethod
	def _serialize_user_content(
		content: str | list[ContentPartTextParam | ContentPartImageParam],
	) -> str | list[str | dict]:
		"""Convert user message content for LangChain compatibility."""
		if isinstance(content, str):
			return content

		serialized_parts = []
		for part in content:
			if part.type == 'text':
				serialized_parts.append(
					{
						'type': 'text',
						'text': part.text,
					}
				)
			elif part.type == 'image_url':
				# LangChain format for images
				serialized_parts.append(
					{'type': 'image_url', 'image_url': {'url': part.image_url.url, 'detail': part.image_url.detail}}
				)

		return serialized_parts

	@staticmethod
	def _serialize_system_content(
		content: str | list[ContentPartTextParam],
	) -> str:
		"""Convert system message content to text string for LangChain compatibility."""
		if isinstance(content, str):
			return content

		text_parts = []
		for part in content:
			if part.type == 'text':
				text_parts.append(part.text)

		return '\n'.join(text_parts)

	@staticmethod
	def _serialize_assistant_content(
		content: str | list[ContentPartTextParam | ContentPartRefusalParam] | None,
	) -> str:
		"""Convert assistant message content to text string for LangChain compatibility."""
		if content is None:
			return ''
		if isinstance(content, str):
			return content

		text_parts = []
		for part in content:
			if part.type == 'text':
				text_parts.append(part.text)
			# elif part.type == 'refusal':
			# 	# Include refusal content as text
			# 	text_parts.append(f'[Refusal: {part.refusal}]')

		return '\n'.join(text_parts)

	@staticmethod
	def _serialize_tool_call(tool_call: ToolCall) -> LangChainToolCall:
		"""Convert browser-use ToolCall to LangChain ToolCall."""
		# Parse the arguments string to a dict for LangChain
		try:
			args_dict = json.loads(tool_call.function.arguments)
		except json.JSONDecodeError:
			# If parsing fails, wrap in a dict
			args_dict = {'arguments': tool_call.function.arguments}

		return LangChainToolCall(
			name=tool_call.function.name,
			args=args_dict,
			id=tool_call.id,
		)

	# region - Serialize overloads
	@overload
	@staticmethod
	def serialize(message: UserMessage) -> HumanMessage: ...

	@overload
	@staticmethod
	def serialize(message: BrowserUseSystemMessage) -> SystemMessage: ...

	@overload
	@staticmethod
	def serialize(message: AssistantMessage) -> AIMessage: ...

	@staticmethod
	def serialize(message: BaseMessage) -> LangChainBaseMessage:
		"""Serialize a browser-use message to a LangChain message."""

		if isinstance(message, UserMessage):
			content = LangChainMessageSerializer._serialize_user_content(message.content)
			return HumanMessage(content=content, name=message.name)

		elif isinstance(message, BrowserUseSystemMessage):
			content = LangChainMessageSerializer._serialize_system_content(message.content)
			return SystemMessage(content=content, name=message.name)

		elif isinstance(message, AssistantMessage):
			# Handle content
			content = LangChainMessageSerializer._serialize_assistant_content(message.content)

			# For simplicity, we'll ignore tool calls in LangChain integration
			# as requested by the user
			return AIMessage(
				content=content,
				name=message.name,
			)

		else:
			raise ValueError(f'Unknown message type: {type(message)}')

	@staticmethod
	def serialize_messages(messages: list[BaseMessage]) -> list[LangChainBaseMessage]:
		"""Serialize a list of browser-use messages to LangChain messages."""
		return [LangChainMessageSerializer.serialize(m) for m in messages]


# Change: Improve Deterministic Tests (Isolate From Repo State)

## Problem

Some unit tests (e.g., ChangeCommand.show/validate) read the live repository
state via `process.cwd()` and `openspec/changes`. This makes outcomes depend on
whatever directories happen to exist and the order returned by `fs.readdir`,
causing flaky success/failure across environments.

Symptoms observed:
- Tests sometimes select a partial or unrelated change folder.
- Failures like missing `proposal.md` when a stray change directory is picked.
- Environment/sandbox differences alter `readdir` ordering and worker behavior.

## Goals

- Make tests deterministic and hermetic.
- Remove dependence on real repo contents and directory ordering.
- Keep runtime behavior unchanged for end users.

## Non‑Goals

- Introduce heavy frameworks or test harness complexity.
- Redesign CLI behavior or change default paths for users.

## Approach

1) Test-local fixture root
- Each suite that touches filesystem discovery creates a temporary directory:
  - `openspec/changes/sample-change/proposal.md`
  - `openspec/changes/sample-change/specs/sample/spec.md`
- `beforeAll`: `process.chdir(tmpRoot)`; `afterAll`: restore original cwd.
- Use a constant `changeName = 'sample-change'`; remove reliance on
  `readdir` order.

2) Optional thin DI for commands (minimal, if needed)
- Allow `ChangeCommand` (and similar) to accept an optional `root` path
  (default `process.cwd()`), used for path resolution.
- Tests pass the temp root explicitly; production code remains unchanged.

3) Harden discovery helpers (safe enhancement)
- Update `getActiveChangeIds()`/`getActiveChanges()` to include only
  directories containing `proposal.md` (and optionally at least one
  `specs/*/spec.md`).
- Prevents incomplete/stray change folders from being treated as active.

## Rationale

- Small, focused changes eliminate flakiness without altering user workflows.
- Temporary fixtures are a well-understood testing pattern and keep tests fast.
- Optional constructor root param is a minimal DI surface that avoids global
  stubbing and keeps code simple.

## Risks & Mitigations

- Risk: Tests forget to restore `process.cwd()`.
  - Mitigation: Add `afterAll` guard restoring cwd; reset `process.exitCode` in
    `afterEach` where modified.
- Risk: Behavior divergence if DI root is misused.
  - Mitigation: Default to `process.cwd()`; only tests pass custom roots.

## Acceptance Criteria

- Tests that previously depended on repo state now:
  - Create and use a temp fixture root.
  - Do not read real `openspec/changes` during execution.
  - Pass consistently regardless of directory order or stray folders.
- No change to CLI behavior for end users (paths still default to cwd).

## Rollout

- Phase 1: Convert the suites that hit `ChangeCommand.show/validate` to
  isolated fixtures; verify stability locally and in CI.
- Phase 2: Apply the same pattern to any remaining suites that touch file
  discovery (`list`, `show`, `validate`, `diff`).
- Phase 3 (optional): Introduce the constructor `root` param and discovery
  hardening, if Phase 1 alone isn’t sufficient.


"use client"

import * as React from "react"
import useEmblaCarousel, {
  type UseEmblaCarouselType,
} from "embla-carousel-react"

import { cn } from "#/lib/utils.ts"
import { Button } from "#/components/ui/button.tsx"
import { HugeiconsIcon } from "@hugeicons/react"
import { ArrowLeft01Icon, ArrowRight01Icon } from "@hugeicons/core-free-icons"

type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]

type CarouselProps = {
  opts?: CarouselOptions
  plugins?: CarouselPlugin
  orientation?: "horizontal" | "vertical"
  setApi?: (api: CarouselApi) => void
}

type CarouselContextProps = {
  carouselRef: ReturnType<typeof useEmblaCarousel>[0]
  api: ReturnType<typeof useEmblaCarousel>[1]
  scrollPrev: () => void
  scrollNext: () => void
  canScrollPrev: boolean
  canScrollNext: boolean
} & CarouselProps

const CarouselContext = React.createContext<CarouselContextProps | null>(null)

function useCarousel() {
  const context = React.useContext(CarouselContext)

  if (!context) {
    throw new Error("useCarousel must be used within a <Carousel />")
  }

  return context
}

function Carousel({
  orientation = "horizontal",
  opts,
  setApi,
  plugins,
  className,
  children,
  ...props
}: React.ComponentProps<"div"> & CarouselProps) {
  const [carouselRef, api] = useEmblaCarousel(
    {
      ...opts,
      axis: orientation === "horizontal" ? "x" : "y",
    },
    plugins
  )
  const [canScrollPrev, setCanScrollPrev] = React.useState(false)
  const [canScrollNext, setCanScrollNext] = React.useState(false)

  const onSelect = React.useCallback((api: CarouselApi) => {
    if (!api) return
    setCanScrollPrev(api.canScrollPrev())
    setCanScrollNext(api.canScrollNext())
  }, [])

  const scrollPrev = React.useCallback(() => {
    api?.scrollPrev()
  }, [api])

  const scrollNext = React.useCallback(() => {
    api?.scrollNext()
  }, [api])

  const handleKeyDown = React.useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
      if (event.key === "ArrowLeft") {
        event.preventDefault()
        scrollPrev()
      } else if (event.key === "ArrowRight") {
        event.preventDefault()
        scrollNext()
      }
    },
    [scrollPrev, scrollNext]
  )

  React.useEffect(() => {
    if (!api || !setApi) return
    setApi(api)
  }, [api, setApi])

  React.useEffect(() => {
    if (!api) return
    onSelect(api)
    api.on("reInit", onSelect)
    api.on("select", onSelect)

    return () => {
      api?.off("select", onSelect)
    }
  }, [api, onSelect])

  return (
    <CarouselContext.Provider
      value={{
        carouselRef,
        api: api,
        opts,
        orientation:
          orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
        scrollPrev,
        scrollNext,
        canScrollPrev,
        canScrollNext,
      }}
    >

        {children}

    </CarouselContext.Provider>
  )
}

function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
  const { carouselRef, orientation } = useCarousel()

  return (

  )
}

function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
  const { orientation } = useCarousel()

  return (

  )
}

function CarouselPrevious({
  className,
  variant = "outline",
  size = "icon-sm",
  ...props
}: React.ComponentProps<typeof Button>) {
  const { orientation, scrollPrev, canScrollPrev } = useCarousel()

  return (
    <Button
      data-slot="carousel-previous"
      variant={variant}
      size={size}
      className={cn(
        "absolute touch-manipulation rounded-full",
        orientation === "horizontal"
          ? "top-1/2 -left-12 -translate-y-1/2"
          : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
        className
      )}
      disabled={!canScrollPrev}
      onClick={scrollPrev}
      {...props}
    >
      <HugeiconsIcon icon={ArrowLeft01Icon} strokeWidth={2} />
      Previous slide
    </Button>
  )
}

function CarouselNext({
  className,
  variant = "outline",
  size = "icon-sm",
  ...props
}: React.ComponentProps<typeof Button>) {
  const { orientation, scrollNext, canScrollNext } = useCarousel()

  return (
    <Button
      data-slot="carousel-next"
      variant={variant}
      size={size}
      className={cn(
        "absolute touch-manipulation rounded-full",
        orientation === "horizontal"
          ? "top-1/2 -right-12 -translate-y-1/2"
          : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
        className
      )}
      disabled={!canScrollNext}
      onClick={scrollNext}
      {...props}
    >
      <HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} />
      Next slide
    </Button>
  )
}

export {
  type CarouselApi,
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
  useCarousel,
}


import re
import pytest
import pytest_httpbin

from scrapling.engines._browsers._stealth import StealthySession, __CF_PATTERN__

class TestStealthConstants:
    """Test Stealth constants and patterns"""

    def test_cf_pattern_regex(self):
        """Test __CF_PATTERN__ regex compilation"""

        assert isinstance(__CF_PATTERN__, re.Pattern)

        # Test matching URLs
        test_urls = [
            "https://challenges.cloudflare.com/cdn-cgi/challenge-platform/h/123456",
            "https://challenges.cloudflare.com/cdn-cgi/challenge-platform/orchestrate/jsch/v1",
            "http://challenges.cloudflare.com/cdn-cgi/challenge-platform/scripts/abc"
        ]

        for url in test_urls:
            assert __CF_PATTERN__.search(url) is not None

        # Test non-matching URLs
        non_matching_urls = [
            "https://example.com/challenge",
            "https://cloudflare.com/something",
            "https://challenges.cloudflare.com/other-path"
        ]

        for url in non_matching_urls:
            assert __CF_PATTERN__.search(url) is None

@pytest_httpbin.use_class_based_httpbin
class TestStealthySession:

    """All the code is tested in the async version tests, so no need to repeat it here. The async class inherits from this one."""
    @pytest.fixture(autouse=True)
    def setup_urls(self, httpbin):
        """Fixture to set up URLs for testing"""
        self.status_200 = f"{httpbin.url}/status/200"
        self.status_404 = f"{httpbin.url}/status/404"
        self.status_501 = f"{httpbin.url}/status/501"
        self.basic_url = f"{httpbin.url}/get"
        self.html_url = f"{httpbin.url}/html"
        self.delayed_url = f"{httpbin.url}/delay/10"  # 10 Seconds delay response
        self.cookies_url = f"{httpbin.url}/cookies/set/test/value"

    def test_session_creation(self):
        """Test if the session is created correctly"""

        with StealthySession(
            headless=True,
            disable_resources=True,
            solve_cloudflare=True,
            wait=1000,
            timeout=60000,
            cookies=[{"name": "test", "value": "123", "domain": "example.com", "path": "/"}],
        ) as session:

            assert session.max_pages == 1
            assert session._config.headless is True
            assert session._config.disable_resources is True
            assert session._config.solve_cloudflare is True
            assert session._config.wait == 1000
            assert session._config.timeout == 60000
            assert session.context is not None

            # Test Cloudflare detection
            for cloudflare_type in ('managed', 'interactive', 'non-interactive'):
                page_content = f"""

                """
                result = session._detect_cloudflare(page_content)
                assert result == cloudflare_type

            page_content = """

Regular page content

            """

            result = StealthySession._detect_cloudflare(page_content)
            assert result is None
            assert session.fetch(self.status_200).status == 200