mirror of
https://github.com/rustfs/rustfs.git
synced 2026-03-17 14:24:08 +00:00
Compare commits
2 Commits
be89b5fc6a
...
ce1f7cfdcb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce1f7cfdcb | ||
|
|
c66c6d97ec |
78
.agents/skills/code-change-verification/SKILL.md
Normal file
78
.agents/skills/code-change-verification/SKILL.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: code-change-verification
|
||||
description: Verify code changes by identifying correctness, regression, security, and performance risks from diffs or patches, then produce prioritized findings with file/line evidence and concrete fixes. Use when reviewing commits, PRs, and merged patches before/after release.
|
||||
---
|
||||
|
||||
# Code Change Verification
|
||||
|
||||
Use this skill to review code changes consistently before merge, before release, and during incident follow-up.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Read the scope: commit, PR, patch, or file list.
|
||||
2. Map each changed area by risk and user impact.
|
||||
3. Inspect each risky change in context.
|
||||
4. Report findings first, ordered by severity.
|
||||
5. Close with residual risks and verification recommendations.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### 1) Scope and assumptions
|
||||
- Confirm change source (diff, commit, PR, files), target branch, language/runtime, and version.
|
||||
- If context is missing, state assumptions before deeper analysis.
|
||||
- Focus only on requested scope; avoid reviewing unrelated files.
|
||||
|
||||
### 2) Risk map
|
||||
- Prioritize in this order:
|
||||
- Data correctness and user-visible behavior
|
||||
- API/contract compatibility
|
||||
- Security and authz/authn boundaries
|
||||
- Concurrency and lifecycle correctness
|
||||
- Performance and resource usage
|
||||
- Give higher priority to stateful paths, migration logic, defaults, and error handling.
|
||||
|
||||
### 3) Evidence-based inspection
|
||||
- Read each modified hunk with neighboring context.
|
||||
- Trace call paths and call-site expectations.
|
||||
- Check for:
|
||||
- invariant breaks and missing guards
|
||||
- unchecked assumptions and null/empty/error-path handling
|
||||
- stale tests, fixtures, and configs
|
||||
- hidden coupling to shared helpers/constants/features
|
||||
- If a point is uncertain, mark it as an open question instead of guessing.
|
||||
|
||||
### 4) Findings-first output
|
||||
- Order findings by severity:
|
||||
- P0: critical failure, security breach, or data loss risk
|
||||
- P1: high-impact regression
|
||||
- P2: medium risk correctness gap
|
||||
- P3: low risk/quality debt
|
||||
- For each finding include:
|
||||
- Severity
|
||||
- `path:line` reference
|
||||
- concise issue statement
|
||||
- impact and likely failure mode
|
||||
- specific fix or mitigation
|
||||
- validation step to confirm
|
||||
- If no issues exist, explicitly state `No findings` and why.
|
||||
|
||||
### 5) Close
|
||||
- Report assumptions and unknowns.
|
||||
- Suggest targeted checks (tests, canary checks, logs/metrics, migration validation).
|
||||
|
||||
## Output Template
|
||||
|
||||
1. Findings
|
||||
2. No findings (if applicable)
|
||||
3. Assumptions / Unknowns
|
||||
4. Recommended verification steps
|
||||
|
||||
## Finding Template
|
||||
|
||||
- `[P1] Missing timeout for downstream call`
|
||||
- Location: `path/to/file.rs:123`
|
||||
- Issue: ...
|
||||
- Impact: ...
|
||||
- Fix suggestion: ...
|
||||
- Validation: ...
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Code Change Verification"
|
||||
short_description: "Prioritize risks and verify code changes before merge."
|
||||
default_prompt: "Inspect a patch or diff, identify correctness/security/regression risks, and return prioritized findings with file/line evidence and fixes."
|
||||
88
.agents/skills/pr-creation-checker/SKILL.md
Normal file
88
.agents/skills/pr-creation-checker/SKILL.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: pr-creation-checker
|
||||
description: Prepare PR-ready diffs by validating scope, checking required verification steps, drafting a compliant English PR title/body, and surfacing blockers before opening or updating a pull request in RustFS.
|
||||
---
|
||||
|
||||
# PR Creation Checker
|
||||
|
||||
Use this skill before `gh pr create`, before `gh pr edit`, or when reviewing whether a branch is ready for PR.
|
||||
|
||||
## Read sources of truth first
|
||||
|
||||
- Read `AGENTS.md`.
|
||||
- Read `.github/pull_request_template.md`.
|
||||
- Use `Makefile` and `.config/make/` for local quality commands.
|
||||
- Use `.github/workflows/ci.yml` for CI expectations.
|
||||
- Do not restate long command matrices or template sections from memory when the files exist.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Collect PR context
|
||||
- Confirm base branch, current branch, change goal, and scope.
|
||||
- Confirm whether the task is: draft a new PR, update an existing PR, or preflight-check readiness.
|
||||
- Confirm whether the branch includes only intended changes.
|
||||
|
||||
2. Inspect change scope
|
||||
- Review the diff and summarize what changed.
|
||||
- Call out unrelated edits, generated artifacts, logs, or secrets as blockers.
|
||||
- Mark risky areas explicitly: auth, storage, config, network, migrations, breaking changes.
|
||||
|
||||
3. Verify readiness requirements
|
||||
- Require `make pre-commit` before marking the PR ready.
|
||||
- If `make` is unavailable, use the equivalent commands from `.config/make/`.
|
||||
- Add scope-specific verification commands when the changed area needs more than the baseline.
|
||||
- If required checks fail, stop and return `BLOCKED`.
|
||||
|
||||
4. Draft PR metadata
|
||||
- Write the PR title in English using Conventional Commits and keep it within 72 characters.
|
||||
- If a generic PR workflow suggests a different title format, ignore it and follow the repository rule instead.
|
||||
- In RustFS, do not use tool-specific prefixes such as `[codex]` when the repository requires Conventional Commits.
|
||||
- Keep the PR body in English.
|
||||
- Use the exact section headings from `.github/pull_request_template.md`.
|
||||
- Fill non-applicable sections with `N/A`.
|
||||
- Include verification commands in the PR description.
|
||||
- Do not include local filesystem paths in the PR body unless the user explicitly asks for them.
|
||||
- Prefer repo-relative paths, command names, and concise summaries over machine-specific paths such as `/Users/...`.
|
||||
|
||||
5. Prepare reviewer context
|
||||
- Summarize why the change exists.
|
||||
- Summarize what was verified.
|
||||
- Call out risks, rollout notes, config impact, and rollback notes when applicable.
|
||||
- Mention assumptions or missing context instead of guessing.
|
||||
|
||||
6. Prepare CLI-safe output
|
||||
- When proposing `gh pr create` or `gh pr edit`, use `--body-file`, never inline `--body` for multiline markdown.
|
||||
- Return a ready-to-save PR body plus a short title.
|
||||
- If not ready, return blockers first and list the minimum steps needed to unblock.
|
||||
|
||||
## Output format
|
||||
|
||||
### Status
|
||||
- `READY` or `BLOCKED`
|
||||
|
||||
### Title
|
||||
- `<type>(<scope>): <summary>`
|
||||
|
||||
### PR Body
|
||||
- Reproduce the repository template headings exactly.
|
||||
- Fill every section.
|
||||
- Omit local absolute paths unless explicitly required.
|
||||
|
||||
### Verification
|
||||
- List each command run.
|
||||
- State pass/fail.
|
||||
|
||||
### Risks
|
||||
- List breaking changes, config changes, migration impact, or `N/A`.
|
||||
|
||||
## Blocker rules
|
||||
|
||||
- Return `BLOCKED` if `make pre-commit` has not passed.
|
||||
- Return `BLOCKED` if the diff contains unrelated changes that are not acknowledged.
|
||||
- Return `BLOCKED` if required template sections are missing.
|
||||
- Return `BLOCKED` if the title/body is not in English.
|
||||
- Return `BLOCKED` if the title does not follow the repository's Conventional Commit rule.
|
||||
|
||||
## Reference
|
||||
|
||||
- Use [pr-readiness-checklist.md](references/pr-readiness-checklist.md) for a short final pass before opening or editing the PR.
|
||||
4
.agents/skills/pr-creation-checker/agents/openai.yaml
Normal file
4
.agents/skills/pr-creation-checker/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "PR Creation Checker"
|
||||
short_description: "Draft RustFS-ready PRs with checks, template, and blockers."
|
||||
default_prompt: "Inspect a branch or diff, verify required PR checks, and produce a compliant English PR title/body plus blockers or readiness status."
|
||||
@@ -0,0 +1,14 @@
|
||||
# PR Readiness Checklist
|
||||
|
||||
- Confirm the branch is based on current `main`.
|
||||
- Confirm the diff matches the stated scope.
|
||||
- Confirm no secrets, logs, temp files, or unrelated refactors are included.
|
||||
- Confirm `make pre-commit` passed, or document why it could not run.
|
||||
- Confirm extra verification commands are listed for risky changes.
|
||||
- Confirm the PR title uses Conventional Commits and stays within 72 characters.
|
||||
- Confirm the PR title does not use tool-specific prefixes such as `[codex]`.
|
||||
- Confirm the PR body is in English.
|
||||
- Confirm the PR body keeps the exact headings from `.github/pull_request_template.md`.
|
||||
- Confirm non-applicable sections are filled with `N/A`.
|
||||
- Confirm the PR body does not include local absolute paths unless explicitly required.
|
||||
- Confirm multiline GitHub CLI commands use `--body-file`.
|
||||
66
.agents/skills/test-coverage-improver/SKILL.md
Normal file
66
.agents/skills/test-coverage-improver/SKILL.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: test-coverage-improver
|
||||
description: Run project coverage checks, rank high-risk gaps, and propose high-impact tests to improve regression confidence for changed and critical code paths before release.
|
||||
---
|
||||
|
||||
# Test Coverage Improver
|
||||
|
||||
Use this skill when you need a prioritized, risk-aware plan to improve tests from coverage results.
|
||||
|
||||
## Usage assumptions
|
||||
- Focus scope is either changed lines/files, a module, or the whole repository.
|
||||
- Coverage artifact must be generated or provided in a supported format.
|
||||
- If required context is missing, call out assumptions explicitly before proposing work.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Define scope and baseline
|
||||
- Confirm target language, framework, and branch.
|
||||
- Confirm whether the scope is changed files only or full-repo.
|
||||
|
||||
2. Produce coverage snapshot
|
||||
- Rust: `cargo llvm-cov` (or `cargo tarpaulin`) with existing repo config.
|
||||
- JavaScript/TypeScript: `npm test -- --coverage` and read `coverage/coverage-final.json`.
|
||||
- Python: `pytest --cov=<pkg> --cov-report=json` and read `coverage.json`.
|
||||
- Collect total, per-file, and changed-line coverage.
|
||||
|
||||
3. Rank highest-risk gaps
|
||||
- Prioritize changed code, branch coverage gaps, and low-confidence boundaries.
|
||||
- Apply the risk rubric in [coverage-prioritization.md](references/coverage-prioritization.md).
|
||||
- Keep shortlist to 5–8 gaps.
|
||||
- For each gap, capture: file, lines, uncovered branches, and estimated risk score.
|
||||
|
||||
4. Propose high-impact tests
|
||||
- For each shortlisted gap, output:
|
||||
- Intent and expected behavior.
|
||||
- Normal, edge, and failure scenarios.
|
||||
- Assertions and side effects to verify.
|
||||
- Setup needs (fixtures, mocks, integration dependencies).
|
||||
- Estimated effort (`S/M/L`).
|
||||
|
||||
5. Close with validation plan
|
||||
- State which gaps remain after proposals.
|
||||
- Provide concrete verification command and acceptance threshold.
|
||||
- List assumptions or blockers (environment, fixtures, flaky dependencies).
|
||||
|
||||
## Output template
|
||||
|
||||
### Coverage Snapshot
|
||||
- total / branch coverage
|
||||
- changed-file coverage
|
||||
- top missing regions by size
|
||||
|
||||
### Top Gaps (ranked)
|
||||
- `path:line-range` | risk score | why critical
|
||||
|
||||
### Test Proposals
|
||||
- `path:line-range`
|
||||
- Test name
|
||||
- scenarios
|
||||
- assertions
|
||||
- effort
|
||||
|
||||
### Validation Plan
|
||||
- command
|
||||
- pass criteria
|
||||
- remaining risk
|
||||
4
.agents/skills/test-coverage-improver/agents/openai.yaml
Normal file
4
.agents/skills/test-coverage-improver/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Test Coverage Improver"
|
||||
short_description: "Find top uncovered risk areas and propose high-impact tests."
|
||||
default_prompt: "Run coverage checks, identify largest gaps, and recommend highest-impact test cases to improve risk coverage."
|
||||
@@ -0,0 +1,25 @@
|
||||
# Coverage Gap Prioritization Guide
|
||||
|
||||
Use this rubric for each uncovered area.
|
||||
|
||||
Score = (Criticality × 2) + CoverageDebt + (Volatility × 0.5)
|
||||
|
||||
- Criticality:
|
||||
- 5: authz/authn, data-loss, payment/consistency path
|
||||
- 4: state mutation, cache invalidation, scheduling
|
||||
- 3: error handling + fallbacks in user-visible flows
|
||||
- 2: parsing/format conversion paths
|
||||
- 1: logging-only or low-impact utilities
|
||||
|
||||
- CoverageDebt:
|
||||
- 0: 0–5 uncovered lines
|
||||
- 1: 6–20 uncovered lines
|
||||
- 2: 21–40 uncovered lines
|
||||
- 3: 41+ uncovered lines
|
||||
|
||||
- Volatility:
|
||||
- 1: stable legacy code with few recent edits
|
||||
- 2: changed in last 2 releases
|
||||
- 3: touched in last 30 days or currently in active PR
|
||||
|
||||
Sort by score descending, then by business impact.
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
use rustfs_filemeta::{ReplicationStatusType, VersionPurgeStatusType};
|
||||
use s3s::dto::{
|
||||
BucketLifecycleConfiguration, ExpirationStatus, LifecycleExpiration, LifecycleRule, NoncurrentVersionTransition,
|
||||
ObjectLockConfiguration, ObjectLockEnabled, RestoreRequest, Transition, TransitionStorageClass,
|
||||
BucketLifecycleConfiguration, ExpirationStatus, LifecycleExpiration, LifecycleRule, LifecycleRuleAndOperator,
|
||||
NoncurrentVersionTransition, ObjectLockConfiguration, ObjectLockEnabled, RestoreRequest, Transition, TransitionStorageClass,
|
||||
};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
@@ -134,6 +134,25 @@ impl RuleValidate for LifecycleRule {
|
||||
}
|
||||
}
|
||||
|
||||
fn lifecycle_rule_prefix(rule: &LifecycleRule) -> Option<&str> {
|
||||
// Prefer a non-empty legacy prefix; treat an empty legacy prefix as if it were not set
|
||||
if let Some(p) = rule.prefix.as_deref() {
|
||||
if !p.is_empty() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(filter) = rule.filter.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(p) = filter.prefix.as_deref() {
|
||||
return Some(p);
|
||||
}
|
||||
|
||||
filter.and.as_ref().and_then(|and| and.prefix.as_deref())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Lifecycle {
|
||||
async fn has_transition(&self) -> bool;
|
||||
@@ -177,8 +196,11 @@ impl Lifecycle for BucketLifecycleConfiguration {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rule_prefix = &rule.prefix.clone().unwrap_or_default();
|
||||
if prefix.len() > 0 && rule_prefix.len() > 0 && !prefix.starts_with(rule_prefix) && !rule_prefix.starts_with(&prefix)
|
||||
let rule_prefix = lifecycle_rule_prefix(rule).unwrap_or("");
|
||||
if !prefix.is_empty()
|
||||
&& !rule_prefix.is_empty()
|
||||
&& !prefix.starts_with(rule_prefix)
|
||||
&& !rule_prefix.starts_with(prefix)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -297,8 +319,8 @@ impl Lifecycle for BucketLifecycleConfiguration {
|
||||
if rule.status.as_str() == ExpirationStatus::DISABLED {
|
||||
continue;
|
||||
}
|
||||
if let Some(prefix) = rule.prefix.clone() {
|
||||
if !obj.name.starts_with(prefix.as_str()) {
|
||||
if let Some(rule_prefix) = lifecycle_rule_prefix(rule) {
|
||||
if !obj.name.starts_with(rule_prefix) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -414,56 +436,23 @@ impl Lifecycle for BucketLifecycleConfiguration {
|
||||
|
||||
if let Some(ref lc_rules) = self.filter_rules(obj).await {
|
||||
for rule in lc_rules.iter() {
|
||||
if obj.expired_object_deletemarker() {
|
||||
if obj.is_latest && obj.expired_object_deletemarker() {
|
||||
if let Some(expiration) = rule.expiration.as_ref() {
|
||||
if let Some(expired_object_delete_marker) = expiration.expired_object_delete_marker {
|
||||
events.push(Event {
|
||||
action: IlmAction::DeleteVersionAction,
|
||||
rule_id: rule.id.clone().unwrap_or_default(),
|
||||
due: Some(now),
|
||||
noncurrent_days: 0,
|
||||
newer_noncurrent_versions: 0,
|
||||
storage_class: "".into(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(days) = expiration.days {
|
||||
let expected_expiry = expected_expiry_time(mod_time, days /*, date*/);
|
||||
if now.unix_timestamp() >= expected_expiry.unix_timestamp() {
|
||||
events.push(Event {
|
||||
action: IlmAction::DeleteVersionAction,
|
||||
rule_id: rule.id.clone().unwrap_or_default(),
|
||||
due: Some(expected_expiry),
|
||||
noncurrent_days: 0,
|
||||
newer_noncurrent_versions: 0,
|
||||
storage_class: "".into(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.is_latest {
|
||||
if let Some(ref expiration) = rule.expiration {
|
||||
if let Some(expired_object_delete_marker) = expiration.expired_object_delete_marker {
|
||||
if obj.delete_marker && expired_object_delete_marker {
|
||||
let due = expiration.next_due(obj);
|
||||
if let Some(due) = due {
|
||||
if expiration.expired_object_delete_marker.is_some_and(|v| v) {
|
||||
if let Some(due) = expiration.next_due(obj) {
|
||||
if now.unix_timestamp() >= due.unix_timestamp() {
|
||||
events.push(Event {
|
||||
action: IlmAction::DelMarkerDeleteAllVersionsAction,
|
||||
action: IlmAction::DeleteVersionAction,
|
||||
rule_id: rule.id.clone().unwrap_or_default(),
|
||||
due: Some(due),
|
||||
noncurrent_days: 0,
|
||||
newer_noncurrent_versions: 0,
|
||||
storage_class: "".into(),
|
||||
});
|
||||
// Stop after scheduling an expired delete-marker event.
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -694,8 +683,16 @@ impl LifecycleCalculate for LifecycleExpiration {
|
||||
if !obj.is_latest || !obj.delete_marker {
|
||||
return None;
|
||||
}
|
||||
// Check date first (date-based expiration takes priority over days).
|
||||
// A zero unix timestamp means "not set" (default value) and is skipped.
|
||||
if let Some(ref date) = self.date {
|
||||
let expiry_date = OffsetDateTime::from(date.clone());
|
||||
if expiry_date.unix_timestamp() != 0 {
|
||||
return Some(expiry_date);
|
||||
}
|
||||
}
|
||||
match self.days {
|
||||
Some(days) => Some(expected_expiry_time(obj.mod_time.unwrap(), days)),
|
||||
Some(days) => obj.mod_time.map(|mod_time| expected_expiry_time(mod_time, days)),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
@@ -860,6 +857,7 @@ impl Default for TransitionOptions {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use s3s::dto::LifecycleRuleFilter;
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_rejects_non_positive_expiration_days() {
|
||||
@@ -1074,4 +1072,208 @@ mod tests {
|
||||
|
||||
assert_eq!(err.to_string(), ERR_LIFECYCLE_INVALID_RULE_STATUS);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filter_rules_respects_filter_prefix() {
|
||||
let mut filter = LifecycleRuleFilter::default();
|
||||
filter.prefix = Some("prefix".to_string());
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
rules: vec![LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
days: Some(30),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: Some(filter),
|
||||
id: Some("rule".to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let match_obj = ObjectOpts {
|
||||
name: "prefix/file".to_string(),
|
||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).unwrap()),
|
||||
is_latest: true,
|
||||
..Default::default()
|
||||
};
|
||||
let matched = lc.filter_rules(&match_obj).await.unwrap();
|
||||
assert_eq!(matched.len(), 1);
|
||||
|
||||
let non_match_obj = ObjectOpts {
|
||||
name: "other/file".to_string(),
|
||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).unwrap()),
|
||||
is_latest: true,
|
||||
..Default::default()
|
||||
};
|
||||
let not_matched = lc.filter_rules(&non_match_obj).await.unwrap();
|
||||
assert_eq!(not_matched.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filter_rules_respects_filter_and_prefix() {
|
||||
let mut filter = LifecycleRuleFilter::default();
|
||||
|
||||
let mut and = LifecycleRuleAndOperator::default();
|
||||
and.prefix = Some("prefix".to_string());
|
||||
filter.and = Some(and);
|
||||
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
rules: vec![LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
days: Some(30),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: Some(filter),
|
||||
id: Some("rule-and-prefix".to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let match_obj = ObjectOpts {
|
||||
name: "prefix/file".to_string(),
|
||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).unwrap()),
|
||||
is_latest: true,
|
||||
..Default::default()
|
||||
};
|
||||
let matched = lc.filter_rules(&match_obj).await.unwrap();
|
||||
assert_eq!(matched.len(), 1);
|
||||
|
||||
let non_match_obj = ObjectOpts {
|
||||
name: "other/file".to_string(),
|
||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).unwrap()),
|
||||
is_latest: true,
|
||||
..Default::default()
|
||||
};
|
||||
let not_matched = lc.filter_rules(&non_match_obj).await.unwrap();
|
||||
assert_eq!(not_matched.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn expired_object_delete_marker_requires_single_version() {
|
||||
let base_time = OffsetDateTime::from_unix_timestamp(1_000_000).unwrap();
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
rules: vec![LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
days: Some(1),
|
||||
expired_object_delete_marker: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: None,
|
||||
id: Some("rule-expired-del-marker".to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let opts = ObjectOpts {
|
||||
name: "obj".to_string(),
|
||||
mod_time: Some(base_time),
|
||||
is_latest: true,
|
||||
delete_marker: true,
|
||||
num_versions: 2,
|
||||
version_id: Some(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let now = base_time + Duration::days(2);
|
||||
let event = lc.eval_inner(&opts, now, 0).await;
|
||||
assert_eq!(event.action, IlmAction::NoneAction);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn expired_object_delete_marker_deletes_only_delete_marker_after_due() {
|
||||
let base_time = OffsetDateTime::from_unix_timestamp(1_000_000).unwrap();
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
rules: vec![LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
days: Some(1),
|
||||
expired_object_delete_marker: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: None,
|
||||
id: Some("rule-expired-del-marker".to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let opts = ObjectOpts {
|
||||
name: "obj".to_string(),
|
||||
mod_time: Some(base_time),
|
||||
is_latest: true,
|
||||
delete_marker: true,
|
||||
num_versions: 1,
|
||||
version_id: Some(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let now = base_time + Duration::days(2);
|
||||
let event = lc.eval_inner(&opts, now, 0).await;
|
||||
|
||||
assert_eq!(event.action, IlmAction::DeleteVersionAction);
|
||||
assert_eq!(event.due, Some(expected_expiry_time(base_time, 1)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn expired_object_delete_marker_date_based_not_yet_due() {
|
||||
// A date-based rule that has not yet reached its expiry date must not
|
||||
// trigger immediate deletion (unwrap_or(now) must not override the date).
|
||||
let base_time = OffsetDateTime::from_unix_timestamp(1_000_000).unwrap();
|
||||
let future_date = base_time + Duration::days(10);
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
rules: vec![LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
date: Some(future_date.into()),
|
||||
expired_object_delete_marker: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: None,
|
||||
id: Some("rule-date-del-marker".to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let opts = ObjectOpts {
|
||||
name: "obj".to_string(),
|
||||
mod_time: Some(base_time),
|
||||
is_latest: true,
|
||||
delete_marker: true,
|
||||
num_versions: 1,
|
||||
version_id: Some(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// now is before the configured date — must not schedule deletion
|
||||
let now_before = base_time + Duration::days(5);
|
||||
let event_before = lc.eval_inner(&opts, now_before, 0).await;
|
||||
assert_eq!(event_before.action, IlmAction::NoneAction);
|
||||
|
||||
// now is after the configured date — must schedule deletion
|
||||
let now_after = base_time + Duration::days(11);
|
||||
let event_after = lc.eval_inner(&opts, now_after, 0).await;
|
||||
assert_eq!(event_after.action, IlmAction::DeleteVersionAction);
|
||||
assert_eq!(event_after.due, Some(future_date));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user