Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
18 changes: 18 additions & 0 deletions apps/decodex/src/agent/tracker_tool_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,24 @@ struct ProgressCheckpointArgs {
pr_url: Option<String>,
}

#[derive(Debug)]
struct NormalizedProgressCheckpoint {
phase: ExecutionProgressPhase,
focus: String,
next_action: String,
blockers: Vec<String>,
evidence: Vec<String>,
verification: Vec<String>,
head_sha: Option<String>,
branch: Option<String>,
pr_url: Option<String>,
}
impl NormalizedProgressCheckpoint {
fn public_branch(&self, review_context: &ReviewHandoffContext) -> String {
self.branch.clone().unwrap_or_else(|| review_context.branch_name.clone())
}
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct LabelArgs {
Expand Down
166 changes: 149 additions & 17 deletions apps/decodex/src/agent/tracker_tool_bridge/tests/mutation/progress.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#[test]
fn progress_checkpoint_writes_structured_issue_comment() {
fn progress_checkpoint_preserves_private_payload_and_publishes_projection() {
let tracker = FakeTracker::new();
let issue = sample_issue();
let workflow = sample_workflow();
Expand Down Expand Up @@ -42,16 +42,43 @@ fn progress_checkpoint_writes_structured_issue_comment() {
assert_eq!(record.record_type, records::LINEAR_EXECUTION_EVENT_RECORD_TYPE);
assert_eq!(record.event_type, "progress_checkpoint");
assert_eq!(record.phase.as_deref(), Some("implementing"));
assert_eq!(record.summary.as_deref(), Some("Execution phase: implementing."));
assert_eq!(record.branch.as_deref(), Some("x/decodex-1"));
assert_eq!(record.worktree_path.as_deref(), Some(".worktrees/PUB-618"));
assert_eq!(record.pr_url.as_deref(), Some("https://github.com/hack-ink/decodex/pull/12"));
assert!(record.focus.is_none());
assert!(record.next_action.is_none());
assert!(record.blockers.is_none());
assert!(record.evidence.is_none());
assert!(record.verification.is_none());
assert!(record.commit_sha.is_none());

let private_events = bridge_state_store(&bridge)
.list_private_execution_events(TEST_SERVICE_ID, &issue.id, "pub-618-attempt-2-123", 2)
.expect("private checkpoint events should list");

assert_eq!(private_events.len(), 1);
assert_eq!(private_events[0].event_type(), "progress_checkpoint");
assert_eq!(
record.focus.as_deref(),
Some("Wire the new execution-state skill into tracker-driven flows.")
private_events[0].payload()["focus"],
serde_json::json!("Wire the new execution-state skill into tracker-driven flows.")
);
assert_eq!(
record.next_action.as_deref(),
Some("Add the issue_progress_checkpoint runtime tool.")
private_events[0].payload()["next_action"],
serde_json::json!("Add the issue_progress_checkpoint runtime tool.")
);
assert_eq!(
private_events[0].payload()["evidence"],
serde_json::json!(["Research decision favors Linear-backed execution snapshots."])
);
assert_eq!(
private_events[0].payload()["verification"],
serde_json::json!(["Local inventory of active execution-state boundary references completed."])
);
assert_eq!(
private_events[0].payload()["head_sha"],
serde_json::json!(sample_local_repo().head_oid)
);
assert_eq!(record.commit_sha.as_deref(), Some(sample_local_repo().head_oid.as_str()));
assert_eq!(record.branch.as_deref(), Some("x/decodex-1"));
}

#[test]
Expand Down Expand Up @@ -156,11 +183,20 @@ fn progress_checkpoint_normalizes_matching_short_head_sha_to_full_head() {
let record = records::parse_linear_execution_event_record(&tracker.comments.borrow()[0])
.expect("progress checkpoint should be a Linear execution event");

assert_eq!(record.commit_sha.as_deref(), Some(sample_local_repo().head_oid.as_str()));
assert!(record.commit_sha.is_none());

let private_events = bridge_state_store(&bridge)
.list_private_execution_events(TEST_SERVICE_ID, &issue.id, "pub-618-attempt-2-123", 2)
.expect("private checkpoint events should list");

assert_eq!(
private_events[0].payload()["head_sha"],
serde_json::json!(sample_local_repo().head_oid)
);
}

#[test]
fn progress_checkpoint_retries_do_not_duplicate_same_ledger_event() {
fn progress_checkpoint_retries_preserve_private_events_without_duplicate_public_projection() {
let tracker = FakeTracker::new();
let issue = sample_issue();
let workflow = sample_workflow();
Expand Down Expand Up @@ -194,10 +230,85 @@ fn progress_checkpoint_retries_do_not_duplicate_same_ledger_event() {
assert!(first.success);
assert!(second.success);
assert_eq!(tracker.comments.borrow().len(), 1);

let private_events = bridge_state_store(&bridge)
.list_private_execution_events(TEST_SERVICE_ID, &issue.id, "pub-618-attempt-2-123", 2)
.expect("private checkpoint events should list");

assert_eq!(private_events.len(), 2);
}

#[test]
fn progress_checkpoint_rejects_private_public_text_before_write() {
fn progress_checkpoint_public_projection_changes_only_on_material_signal() {
let tracker = FakeTracker::new();
let issue = sample_issue();
let workflow = sample_workflow();
let temp_dir = TempDir::new().expect("tempdir should create");
let pull_request_inspector = FakePullRequestInspector::new(Vec::new());
let local_repo_inspector = FakeLocalRepoInspector::new(vec![Ok(sample_local_repo())]);
let bridge = TrackerToolBridge::with_review_handoff_for_test(
&tracker,
&issue,
&workflow,
sample_review_context_in(temp_dir.path()),
&pull_request_inspector,
&local_repo_inspector,
);
let first = DynamicToolHandler::handle_call(
&bridge,
ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME,
serde_json::json!({
"phase": "implementing",
"focus": "First private implementation focus.",
"next_action": "Continue implementation.",
"blockers": [],
"evidence": ["First private evidence item."],
"head_sha": sample_local_repo().head_oid
}),
);
let non_material = DynamicToolHandler::handle_call(
&bridge,
ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME,
serde_json::json!({
"phase": "implementing",
"focus": "Changed private implementation focus.",
"next_action": "Changed private next action.",
"blockers": [],
"evidence": ["Changed private evidence item."],
"head_sha": sample_local_repo().head_oid
}),
);
let material = DynamicToolHandler::handle_call(
&bridge,
ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME,
serde_json::json!({
"phase": "verifying",
"focus": "Verify the implementation.",
"next_action": "Run tests.",
"blockers": [],
"evidence": ["Implementation reached verification."],
"head_sha": sample_local_repo().head_oid
}),
);

assert!(first.success);
assert!(non_material.success);
assert!(material.success);
assert_eq!(
tracker.comments.borrow().len(),
2,
"private-only changes inside the same public phase must not flood Linear"
);

let private_events = bridge_state_store(&bridge)
.list_private_execution_events(TEST_SERVICE_ID, &issue.id, "pub-618-attempt-2-123", 2)
.expect("private checkpoint events should list");

assert_eq!(private_events.len(), 3);
}

#[test]
fn progress_checkpoint_stores_private_text_but_redacts_public_projection() {
let tracker = FakeTracker::new();
let issue = sample_issue();
let workflow = sample_workflow();
Expand Down Expand Up @@ -225,12 +336,33 @@ fn progress_checkpoint_rejects_private_public_text_before_write() {
}),
);

assert!(!response.success);
assert!(
response
.content_items
.iter()
.any(|item| matches!(item, DynamicToolContentItem::InputText { text } if text.contains("public/team-visible")))
assert!(response.success);
assert_eq!(tracker.comments.borrow().len(), 1);

let body = &tracker.comments.borrow()[0];

assert!(!body.contains("/Users/example/code/private"));
assert!(!body.contains("GITHUB_PAT_Y"));

let record = records::parse_linear_execution_event_record(body)
.expect("public projection should parse as a Linear execution event");

assert_eq!(record.phase.as_deref(), Some("implementing"));
assert_eq!(record.summary.as_deref(), Some("Execution phase: implementing."));
assert!(record.focus.is_none());
assert!(record.next_action.is_none());
assert!(record.evidence.is_none());

let private_events = bridge_state_store(&bridge)
.list_private_execution_events(TEST_SERVICE_ID, &issue.id, "pub-618-attempt-2-123", 2)
.expect("private checkpoint events should list");

assert_eq!(
private_events[0].payload()["focus"],
serde_json::json!("Inspected /Users/example/code/private checkout.")
);
assert_eq!(
private_events[0].payload()["evidence"],
serde_json::json!(["Missing GITHUB_PAT_Y was observed."])
);
assert!(tracker.comments.borrow().is_empty());
}
Loading