* feat(my-skills): add keyword search, namespace filter and clickable pagination
Add comprehensive filtering and search capabilities to the My Skills page:
- Keyword search: search by skill name, slug, or description
- Namespace filter: filter skills by namespace
- Clickable pagination: page number buttons with smart ellipsis
- State preservation: sync search state to URL, restore when returning from detail page
- Debounced search: 300ms debounce to avoid excessive queries
- Fix: hide stale rejected preview badge when newer version is published
Backend changes:
- MySkillAppService: add keyword and namespace filtering logic
- SkillLifecycleProjectionService: only show preview versions newer than published
- MeController: add keyword and namespace query parameters
- 6 new test cases covering search and filter scenarios
Frontend changes:
- my-skills.tsx: search input, namespace dropdown, URL state sync
- pagination.tsx: clickable page numbers with ellipsis
- use-user-queries.ts: prevent flicker on query transitions
- skill-detail.tsx: remove invalid rejected badge display
- router.tsx: URL parameter validation
- i18n: add search-related translation keys
Synced from SAAS commits:
- 939fa749 (feat: search and filters)
- dc14df6c (fix: search flicker)
- 0168ea81 (fix: rejected badge)
- c9eefa93 (fix: stale preview)
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
* fix(tests): address test failures in PR #493
Backend test fixes:
- Remove unnecessary Mockito stubbing for filtered-out skills
- Add missing findBySkillIdAndStatus stub for published version lookup
- Update MeController test mocks to match new method signature (keyword, namespace params)
Frontend fixes:
- Fix TypeScript error: useMyNamespaces returns ManagedNamespace[] not PagedResponse
- Add type annotation for namespace map callback parameter
E2E test fix:
- Update URL regex to allow query parameters (returnTo from search page)
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
* fix(tests): resolve test failures in PR #493
Backend:
- Remove unnecessary mock stubbings for skillId 2 and 3 in MySkillAppServiceTest.listMySkills_combinesKeywordNamespaceAndStatusFilters
- The test filters results to only return skill with id=1, so mocks for id 2 and 3 were never called, causing UnnecessaryStubbingException
Frontend:
- Add missing mocks for useLocation, useSearch, useMyNamespaces, and useDebounce in my-skills.test.ts
- MySkillsPage component uses these hooks but the test setup didn't provide mocks, causing 'No QueryClient set' and 'No export' errors
All 4 frontend tests now pass locally.
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---------
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
The PG FK constraint fk_skill_latest_version blocks deleting a
SkillVersion whenever Skill.latest_version_id still references it.
Two services had the wrong order:
- SkillPublishService.deleteReplaceableVersionArtifacts: triggered
when re-uploading the same version (UPLOADED -> overwritten).
Reproduced by AstronClaw client retrying personal-skills upload.
- SkillGovernanceService.deleteVersion: triggered when admin deletes
a draft version that happens to be skill.latest_version_id.
Fix: clear skill.latest_version_id and flush BEFORE deleting the
SkillVersion row, so PG sees no live reference at delete time.
Synced from SAAS commit 4626f0c117d9c0544c4dc1115c3aac7468f0d277
* feat(cli,domain): support non-global namespace skill download
Parse namespace from skill name using -- separator (e.g.,
astroclaw--api-gateway) so users don't need --namespace flag.
Allow anonymous download for any PUBLIC skill regardless of namespace.
CLI changes:
- Add cli/src/shared/skill-name-parser.ts utility
- Update install and remove commands to parse skill name argument
- 10 unit tests covering edge cases
Domain changes:
- SkillDownloadService.isAnonymousDownloadAllowed: drop namespace
type check, only require PUBLIC visibility
- Update test to expect success for team-namespace public skill
Synced from SAAS commit 26c67e31b1221249cf9b73321d1b726d8ba6e6df
* fix(cli): use bun:test instead of vitest in skill-name-parser test
## Problem
Audit log timestamps displayed 8 hours later than actual time when JVM
default timezone != UTC. Root cause: `audit_log.created_at` was
`TIMESTAMP without time zone`, and `rs.getTimestamp()` interprets bare
values using JVM timezone.
## Solution
### Backend
- **V42 migration**: Upgrade `audit_log.created_at` from `TIMESTAMP` to
`TIMESTAMPTZ`, anchor historical data as UTC via `USING created_at AT
TIME ZONE 'UTC'` (same pattern as V18/V19/V23/V25/V36)
- **Read path**: `AdminAuditLogAppService.readInstant()` uses
`rs.getObject(col, OffsetDateTime.class).toInstant()`, result
independent of JVM timezone
- **Write path (filter params)**: `startTime`/`endTime` binding changed
from `Timestamp.from()` to `OffsetDateTime.ofInstant(instant,
ZoneOffset.UTC)` via `toUtcOffsetDateTime()` helper, symmetric with
read path
### Migration Safety
- `SET LOCAL lock_timeout = '30s'` (transaction-scoped, won't leak to pool)
- `DO $$ ... IF data_type = 'timestamp without time zone' THEN ... ELSE
... END $$` idempotent guard with dual-branch `RAISE NOTICE`
- Safe retry: re-running won't double-apply `AT TIME ZONE 'UTC'`
### Test Coverage (10 tests, 477 total suite)
- `rowMapper_readsCreatedAtAsInstant` — UTC offset regression
- `rowMapper_normalisesNonUtcOffsetToInstant` — Non-UTC offset (+08:00)
- `rowMapper_returnsNullTimestampWhenColumnIsNull` — Null path
- `rowMapper_isIndependentOfJvmDefaultTimezone` — JVM TZ=Asia/Shanghai
drift prevention with `verify(rs, never()).getTimestamp()`
- `@ParameterizedTest buildWhereClause_bindsTimeRangeAsOffsetDateTime` —
3 cases (both/startOnly/endOnly) for filter param binding
- `@BeforeEach setUp()` — Mock isolation to prevent cross-test stub
accumulation
## Quality Gates
- [x] `make test-backend-app` passes (477 tests, 0 failures)
- [x] No Controller changes, `make generate-api` not needed
- [x] No frontend changes, typecheck/lint/e2e not needed
## Deployment
V42 must run before new code (guaranteed by Spring Boot startup sequence
→ Flyway executes before app accepts traffic). Rolling deployment:
- New pod + migrated column: correct
- Old pod + migrated column: old code reads TIMESTAMPTZ correctly (pgjdbc
returns absolute instant)
## Related Docs
- `docs/15-backend-time-governance-plan.md` §3.1: V42 progress registered
- `docs/16-backend-time-inventory.md` §3.1: V42 listed
- Same migration pattern: V18/V19/V23/V25/V36
* fix(auth): use SimpleUrlAuthenticationSuccessHandler for OAuth2 login
Replace SavedRequestAwareAuthenticationSuccessHandler with
SimpleUrlAuthenticationSuccessHandler to prevent redirecting to
saved API requests after OAuth2 login.
Previously, when a user accessed a protected API endpoint (e.g.,
/api/web/skills) without authentication, Spring Security would save
that request. After OAuth2 login, the handler would redirect back to
the API endpoint instead of the dashboard.
Now the handler only uses:
- returnTo parameter from session (if present)
- default target URL (/dashboard) as fallback
* test(auth): add regression for OAuth2 success redirect; restore clearAuthenticationAttributes
Cover the no-returnTo + cached-API-request branch with HttpSessionRequestCache so
the original bug (post-login redirect resolving to /api/web/skills) cannot be
silently reintroduced. Also restore clearAuthenticationAttributes() in the
returnTo branch so it stays symmetric with the default branch (super clears it).
---------
Co-authored-by: xiose <huyanlin@nuaa.edu.cn>
ApiTokenAuthenticationFilter authenticates /api/cli/** Bearer tokens
but ApiTokenScopeFilter.shouldNotFilter() previously skipped them.
The result: API token requests on CLI routes were authenticated and
authorization-policy-checked, but scope enforcement never ran. Tokens
without skill:publish or skill:delete could call /publish, /publish/validate,
and DELETE despite the policy table requiring those scopes.
Add /api/cli/ to the scope filter's covered prefixes and a filter-level
test that confirms a token missing skill:publish is rejected on the new
validate endpoint. Update the existing CLI controller tests to grant
the appropriate SCOPE_* authorities to their api_token principals so
they continue to pass under enforced scopes.
Fix three blockers and one contract drift issue surfaced in code review:
1. API token policy: add skill:publish scope policy and authentication
policy for /api/cli/v1/skills/*/publish/validate. Without these the
AntPathMatcher pattern /publish would not cover /publish/validate,
so Bearer-token requests would be rejected by the scope filter.
2. Warnings semantics: dry-run now treats warnings as making valid=false.
The CLI publish flow uses confirmWarnings=false, so the real publish
rejects any warnings; dry-run must mirror that to avoid false positives.
3. Visibility parameter: validate endpoint now accepts the same
visibility multipart field as publish. The CLI forwards --visibility
so invalid values are caught at dry-run time rather than at publish.
4. Schema drift: resolvedSlug and resolvedVersion are nullable in
practice (returned as null when validation fails before resolution).
Updated schema.d.ts to reflect string | null instead of optional string.
Tests added:
- RouteSecurityPolicyRegistryTest: validate endpoint scope check
- CliDryRunValidateTest: custom + invalid visibility cases
- publish-dry-run.test.ts: --visibility forwarded to server
- Exit non-zero (code 6) when --dry-run validation fails, enabling
CI/CD pipeline integration
- Add archived skill check: dry-run now detects when the publisher's
own skill is archived
- Add version-exists check: dry-run now detects when the resolved
version is already published
- Use StandardCharsets.UTF_8 for SKILL.md content parsing
Add a validate-only endpoint (POST /api/cli/v1/skills/{namespace}/publish/validate)
that runs the full pre-publish validation chain without persisting anything.
This allows developers to check their package locally before actual publishing.
The validation covers:
- SKILL.md existence and frontmatter parsing (name, description required)
- File extension whitelist and size limits
- Credential leak scanning with line-number precision
- Slug generation and name conflict detection
CLI usage: `skillhub publish <path> --dry-run`
Closes#429
When approving a promotion, the new SkillVersion was created without copying
bundleReady and downloadReady from the source version, causing the download
button to be permanently disabled for promoted skills.
Gemini review feedback: the previous != PUBLISHED condition was too broad
and could inadvertently overwrite terminal states like REJECTED or YANKED.
Now explicitly check == SCANNING before transitioning status.
Super admin auto-publish flow was skipping security scanning entirely.
Now triggerScan is called regardless of autoPublish flag, while preserving
the PUBLISHED status (scan runs as post-publish audit rather than blocking).
Closes#415
Resolved conflicts by keeping both sides:
- web/src/api/client.ts: preserve paginated listMembers(slug, {page,size})
and add delete(slug) from main.
- web/src/shared/hooks/use-namespace-queries.ts: keep useUpdateNamespace
and useTransferNamespaceOwnership from this branch, plus useDeleteNamespace
from main.
Implement POST /namespaces/{slug}/transfer-ownership to allow namespace owners to transfer ownership to existing members. Includes comprehensive test coverage for success and failure scenarios (non-owner, target not found, frozen namespace).