Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

Check warning on line 203 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / Trunk Check

yamllint(comments)

[new] too few spaces before comment: expected 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix inline-comment spacing on both pinned checkout lines.

Line 203 and Line 255 have one space before # v7. yamllint reports both lines. Add a second space before the comment.

Proposed fix
-      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7

Apply the same spacing change to Line 255.

Also applies to: 255-255

🧰 Tools
🪛 GitHub Check: Trunk Check

[warning] 203-203: yamllint(comments)
[new] too few spaces before comment: expected 2

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 203, Update both pinned actions/checkout
entries in the CI workflow to use two spaces before the inline “# v7” comments,
including the entries near lines 203 and 255, without changing the pinned commit
or version.

Source: Linters/SAST tools

with:
persist-credentials: false
- name: rootless / no-net SelfCheck
shell: pwsh
run: ./scripts/rootless-nonet-check.ps1 -SelfCheck
Expand Down Expand Up @@ -250,7 +252,9 @@
name: rootless-only matrix policy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

Check warning on line 255 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / Trunk Check

yamllint(comments)

[new] too few spaces before comment: expected 2
with:
persist-credentials: false
- name: assert rootless-only OCI runner matrix scaffold anchors
shell: pwsh
run: ./scripts/rootless-matrix-check.ps1 -SelfCheck
Expand Down
96 changes: 73 additions & 23 deletions crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

use proptest::prelude::*;
use session_ledger::domain::session::{Corpus, Message, Role, Session};
use session_ledger::domain::worklog::{UnfinishedReason, UnfinishedWorkItem};
use session_ledger::domain::worklog::UnfinishedReason;
use sl_viewer::unfinished_tab::{reason_label, unfinished_items};

// ── strategies ─────────────────────────────────────────────────────────────
Expand All @@ -30,19 +30,18 @@ fn session_strategy() -> impl Strategy<Value = Session> {
(
// session_id — non-empty, identifier-shaped.
"[a-zA-Z0-9_-]{1,16}",
// 0..6 messages; mix of roles + content. Bounded so the
// `detect_unfinished` projection runs cheaply.
// 0..6 messages; each message has its own independent
// `Option<i64>` ts_ms so the `detect_unfinished` projection's
// `find_map(|m| m.ts_ms).rev()` contract is exercised naturally
// (per-message timestamps, not a session-wide value).
Comment on lines +33 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Compute the maximum timestamp, not the last timestamped message.

corpus_tab::last_activity_ms uses filter_map(|m| m.ts_ms).max(). Reverse find_map returns the final timestamped message in message order. It does not return the maximum timestamp.

With independently generated timestamps, this property fails for a session such as [Some(20), Some(10)]. Update the comment and expected value to use max().

Proposed fix
-        // `find_map(|m| m.ts_ms).rev()` contract is exercised naturally
+        // maximum-known-timestamp contract is exercised naturally
@@
-            let expected_last_activity_ms =
-                session.messages.iter().rev().find_map(|m| m.ts_ms);
+            let expected_last_activity_ms =
+                session.messages.iter().filter_map(|m| m.ts_ms).max();

Also applies to: 219-225

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs` around lines 33 -
36, Update the unfinished-tab property test around the timestamp generation and
expected activity value to model the implementation’s maximum-timestamp
behavior: replace the reverse find_map expectation with filter_map(...).max()
semantics and revise the nearby comment accordingly. Ensure cases with
independently generated timestamps, such as [Some(20), Some(10)], expect 20
rather than the last timestamped message’s value.

prop::collection::vec(
(0u8..5, "[ -~]{1,40}"),
(0u8..5, "[ -~]{1,40}", prop::option::of(0i64..1_000_000_000_000)),
0..6,
),
// last_activity_ms — Some(i64) or None. None is the "unknown
// last activity" sentinel the worklog projector uses.
prop::option::of(0i64..1_000_000_000_000),
)
.prop_map(|(session_id, messages, ts_ms)| {
.prop_map(|(session_id, messages)| {
let mut session = Session::new(format!("sess-{session_id}"), Corpus::Forge);
for (role_idx, content) in messages {
for (role_idx, content, ts_ms) in messages {
let role = match role_idx % 5 {
0 => Role::User,
1 => Role::Assistant,
Expand Down Expand Up @@ -109,26 +108,46 @@ proptest! {
}

/// Property: `unfinished_items` orders known timestamps descending.
/// Two items with the same `last_activity_ms` may appear in any order
/// (we don't constrain the tiebreak here; see next property).
/// Two invariants:
/// (a) within the sliding window of items with a known timestamp,
/// timestamps are non-increasing;
/// (b) once an item with `last_activity_ms == None` appears, no
/// later item may carry a known timestamp (None is the
/// "unknown last activity" sentinel and always sorts last).
#[test]
fn unfinished_items_orders_known_timestamps_descending(
sessions in prop::collection::vec(session_strategy(), 1..10),
) {
let items = unfinished_items(&sessions);

// Filter to items with a known timestamp so the descending
// invariant applies cleanly.
let with_ts: Vec<&UnfinishedWorkItem> =
items.iter().filter(|i| i.last_activity_ms.is_some()).collect();

for window in with_ts.windows(2) {
let prev = window[0].last_activity_ms.expect("filtered Some");
let next = window[1].last_activity_ms.expect("filtered Some");
prop_assert!(
prev >= next,
"known timestamps must be descending: {prev} came before {next}",
);
// (a) descending among items with a known timestamp.
for window in items.windows(2) {
let prev = &window[0];
let next = &window[1];
match (prev.last_activity_ms, next.last_activity_ms) {
(Some(a), Some(b)) => {
prop_assert!(
a >= b,
"known timestamps must be non-increasing: {a} came before {b}",
);
}
_ => {}
}
}

// (b) no Some(ts) appears after a None.
let mut seen_none = false;
for item in &items {
if seen_none {
prop_assert!(
item.last_activity_ms.is_none(),
"Some(ts) found after None: {:?} appeared after a None item",
item.last_activity_ms,
);
}
if item.last_activity_ms.is_none() {
seen_none = true;
}
}
}

Expand Down Expand Up @@ -177,4 +196,35 @@ proptest! {
"appending sessions must not lose items: base={base_items}, combined={combined_items}",
);
}

/// Property: for each projected item, `last_activity_ms` equals the
/// maximum known `ts_ms` over the *session's* messages, or `None`
/// if none of the session's messages carried a timestamp. This pins
/// the per-message → projected-Item reduction explicitly (the unit
/// tests in `domain/worklog.rs` cover specific values; this property
/// pins the projection over many shapes).
#[test]
fn unfinished_items_last_activity_matches_session_max_ts(
sessions in prop::collection::vec(session_strategy(), 0..8),
) {
let items = unfinished_items(&sessions);

for item in &items {
// Reconstruct the source session by id.
let session = sessions
.iter()
.find(|s| s.id == item.session_id)
.expect("projected item must reference an input session");
Comment on lines +214 to +217

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: session_strategy can generate duplicate session IDs, but this reconstruction selects the first matching session for every projected item. When duplicate sessions have different timestamps and both are unfinished, the second item's projection is compared with the first session's timestamp and the property fails spuriously. Ensure generated IDs are unique or preserve the source-session identity when comparing projections. [incorrect variable usage]

Severity Level: Major ⚠️
- ❌ Property test can fail for valid duplicate-ID inputs.
- ⚠️ CI reliability depends on generated session IDs remaining unique.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs
**Line:** 214:217
**Comment:**
	*Incorrect Variable Usage: `session_strategy` can generate duplicate session IDs, but this reconstruction selects the first matching session for every projected item. When duplicate sessions have different timestamps and both are unfinished, the second item's projection is compared with the first session's timestamp and the property fails spuriously. Ensure generated IDs are unique or preserve the source-session identity when comparing projections.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


let expected_last_activity_ms =
session.messages.iter().rev().find_map(|m| m.ts_ms);
Comment on lines +219 to +220

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The property claims to verify the maximum timestamp, but iter().rev().find_map(...) returns the last timestamped message in message order, not the numerically greatest timestamp. As a result, a regression that changes production to use an incorrect numeric maximum could pass this property, while the property description and assertion message incorrectly claim maximum semantics. Either rename the property/documentation to “latest timestamped message” or compute an actual maximum if that is the intended contract. [docstring mismatch]

Severity Level: Minor 🧹
- ⚠️ Property documentation misstates the timestamp contract.
- ⚠️ Numeric-maximum regressions are not detected by this test.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs
**Line:** 219:220
**Comment:**
	*Docstring Mismatch: The property claims to verify the maximum timestamp, but `iter().rev().find_map(...)` returns the last timestamped message in message order, not the numerically greatest timestamp. As a result, a regression that changes production to use an incorrect numeric maximum could pass this property, while the property description and assertion message incorrectly claim maximum semantics. Either rename the property/documentation to “latest timestamped message” or compute an actual maximum if that is the intended contract.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


prop_assert_eq!(
item.last_activity_ms,
expected_last_activity_ms,
"session {}: projected last_activity_ms must equal session max known ts_ms",
session.id,
);
}
}
}
9 changes: 8 additions & 1 deletion scripts/rootless-matrix-check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,14 @@ if ($securityWf -match '(?ms)^ rootless-matrix:(.*?)(?=^ [a-z][a-z0-9-]*:)') {
}
[void](Write-Check -Label "security.yml rootless-matrix job is blocking when present" -Ok $true)

if ($ciWf -match '(?ms)^ rootless-matrix-policy:.*?continue-on-error:\s*true') {
if (-not ($ciWf -match '(?ms)^ rootless-matrix-policy:')) {
throw "ci.yml must define a rootless-matrix-policy job block (C04 L40 cross-reference anchor)."
}
$matrixPolicyBlockMatch = [regex]::Match(
$ciWf,
'(?ms)^ rootless-matrix-policy:.*?(?=^ [A-Za-z][\w-]*:\s|\z)'
)
if ($matrixPolicyBlockMatch.Success -and $matrixPolicyBlockMatch.Value -match 'continue-on-error:\s*true') {
throw "ci.yml rootless-matrix-policy job must be blocking (no continue-on-error)."
}
[void](Write-Check -Label "ci.yml rootless-matrix-policy job is blocking when present" -Ok $true)
Expand Down
5 changes: 4 additions & 1 deletion scripts/rootless-nonet-check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ $policyBlockMatch = [regex]::Match(
$ciWf,
'(?ms)^ rootless-nonet-policy:.*?(?=^ [A-Za-z][\w-]*:\s|\z)'
)
if ($policyBlockMatch.Success -and $policyBlockMatch.Value -match 'continue-on-error:\s*true') {
if (-not $policyBlockMatch.Success) {
throw "ci.yml must define a rootless-nonet-policy job block (C04 L40 cross-reference anchor)."
}
if ($policyBlockMatch.Value -match 'continue-on-error:\s*true') {
throw "ci.yml rootless-nonet-policy job must be blocking (no continue-on-error)."
}
[void](Write-Check -Label "ci.yml rootless-nonet-policy job is blocking when present" -Ok $true)
Expand Down
15 changes: 9 additions & 6 deletions tests/alloc_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,15 @@ fn alloc_profile_script_self_check_parses_args_and_ceilings() {
assert!(stdout.contains("Profiler: dhat"), "expected profiler echo, got:\n{stdout}");
}
Err(error) => {
// Windows can't fall back to a portable load + print, so the
// spawn failure is unrecoverable there. Other targets run the
// portable fallback below. The clippy `panic_in_if_then` lint
// requires the if-then to have an else branch — fold the
// fallback into `else` so the panic sits on the windows-only path.
// On non-Windows targets, a `NotFound` error means pwsh isn't
// installed — fall back to the portable SelfCheck. Any other
// I/O error (permission, broken pipe, etc.) is treated as a
// hard failure since it likely means the test environment is
// misconfigured (e.g., spawn denial, missing CWD). Windows has
// no portable fallback path, so any spawn failure is fatal.
if cfg!(target_os = "windows") {
panic!("failed to spawn pwsh for self-check: {error}");
} else {
} else if error.kind() == std::io::ErrorKind::NotFound {
let (max_bytes, total_blocks) = load_profile();
println!(
"pwsh unavailable; running portable alloc-profile SelfCheck fallback.\nSelf-check passed\nMax bytes ceiling: {max_bytes}\nTotal blocks ceiling: {total_blocks}\nProfiler: dhat"
Expand All @@ -102,6 +103,8 @@ fn alloc_profile_script_self_check_parses_args_and_ceilings() {
total_blocks >= 1_000,
"total_blocks ceiling should stay generous (got {total_blocks})"
);
} else {
panic!("failed to spawn pwsh for self-check (non-NotFound): {error}");
}
}
}
Expand Down
Loading