Skip to content

Outline Operation Test Fixes - Phase 4 Alignment#275

Merged
jsavin merged 4 commits into
developfrom
feature/outline-test-fixes
Jan 11, 2026
Merged

Outline Operation Test Fixes - Phase 4 Alignment#275
jsavin merged 4 commits into
developfrom
feature/outline-test-fixes

Conversation

@jsavin

@jsavin jsavin commented Jan 11, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes outline operation test failures by documenting essential outline architecture and aligning test expectations with UserTalk conventions and headless mode behavior.

Key Changes:

  • Documented empty summit behavior (all outlines start with 1 empty headline)
  • Fixed op.firstSummit() to use opmoveto(hsummit) - always returns true
  • Aligned test expectations with UserTalk type coercion conventions
  • Documented cursor visibility invariant (cursor auto-moves when parent collapsed)

Commits:

  • 6ea371ff - fix: Phase 4 state management verbs - expansion state, refcon, and documentation
  • 6ce0dd24 - fix: Align outline operation tests with empty summit behavior and fix cursor invariants
  • 5eeb6c7d - fix: Correct test docs: capture headless daemon vision #21 expectation - script_error not json_parse_error

Test Results

✅ Unit tests: All passing (./tools/run_headless_tests.sh)

Integration tests not run yet - will be validated by CI.

Key Files Changed

Documentation:

  • docs/OUTLINE_STRUCTURE.md - NEW: Critical architecture documentation
  • planning/phase3/OUTLINE_EMPTY_SUMMIT_FIX.md - NEW: Implementation details
  • planning/phase3/phase4_test_failures.md - NEW: Test failure analysis

Implementation:

  • tests/headless_op_verbs.c - Fixed op.firstSummit() implementation

Tests:

Context

Empty Summit Behavior

All new outlines start with exactly 1 empty headline (the summit). This is intentional design.

Cursor Visibility Invariant

When a parent node is collapsed, the cursor cannot remain on its children. The runtime automatically moves the cursor to the first visible ancestor.

Type Coercion

UserTalk uses type coercion (1 → true, 0 → false), not strict type checking.

Test #21 Verification

Tested in Windows Frontier: passing a string to op.setExpansionState() produces a script error (type coercion failure), not a JSON parse error.

Related Issues


🤖 Generated with Claude Code

jsavin and others added 3 commits January 11, 2026 00:29
…cumentation

Fixed P0 expansion state save/restore issues and improved state management verbs:

- Fixed op.setExpansionState to restore state after collapse (tests 3, 7, 17)
- Root cause: Checked (**nomad).flexpanded flag which gets cleared by opcollapse()
- Solution: Only check !opsubheadsexpanded() to allow re-expansion
- Tests now passing: Basic restore, workflow restore, deep nesting stress test

- Fixed op.promote/demote to use oppromote()/opdemote() instead of opreorgcursor()
  - Properly handles multiple nodes and preserves refcon data (test 14)
- Fixed op.setCursor to use opmoveto() for proper state updates (test 15)
- Made scroll state operations noops in headless mode (tests 5, 13)

- Created docs/OUTLINE_STRUCTURE.md documenting critical architectural fact:
  All new outlines start with 1 empty summit headline
- Updated CLAUDE.md with outline structure quick reference
- Documented memory logging false alarms (format detection, not corruption)
- Created planning/phase3/phase4_test_failures.md analyzing all test failures

- Updated scroll state test expectations for headless mode behavior
- Fixed workflow test ordering issues

Test results: 537/600 passing (89.5%), P0 expansion state issues resolved

Related: Issues #272, #273 filed for future expansion state design improvements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… cursor invariants

This commit addresses outline operation test failures by properly documenting
and implementing the empty summit behavior, fixing op.firstSummit(), and
correcting test expectations to match UserTalk conventions and headless mode.

- **docs/OUTLINE_STRUCTURE.md**: Added critical section documenting cursor and
  collapsed nodes invariant - the cursor can NEVER be on a child of a collapsed
  node. When expansion state is restored, cursor auto-moves to first visible
  ancestor if needed.

- **planning/phase3/OUTLINE_EMPTY_SUMMIT_FIX.md**: Documented the empty summit
  issue and historical context (file was created in previous work but uncommitted).

- **tests/headless_op_verbs.c**: Fixed op.firstSummit() to explicitly move to
  hsummit using opmoveto() instead of opmotionkey(flatup, infinity), and always
  return true (opmoveto returns false when already at target, causing test failures).

- **tests/integration/test_cases/op_verbs.yaml**:
  - Fixed test #11 (cursor independence): Rewrote to properly test that expansion
    state captures entire outline, accounting for cursor auto-move when parent is
    collapsed
  - Fixed type safety tests based on UserTalk conventions:
    - Test #19 (setCursor): Expects false when invalid cursor passed
    - Test #20 (setDisplay): UserTalk type coercion (1 → true) is correct behavior
    - Test #21 (setExpansionState): Expects json_parse_error for non-list
    - Test #22 (setScrollState): Noop in headless, returns true
    - Test #23 (setRefcon): Split into 4 tests proving round-trip for boolean,
      string, long, and double (refcons can store any type)

Following Phase 4 state management implementation, tests revealed misunderstandings
about:
1. Empty summit behavior (all outlines start with 1 empty headline)
2. Cursor visibility invariant (cursor must always be on accessible/visible node)
3. UserTalk type coercion vs strict type checking expectations

This commit documents these behaviors and aligns tests with reality.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When passing a non-list type to op.setExpansionState(), it attempts type
coercion and fails with a script error: 'Can't coerce the string "foo" into
a number because it contains non-numeric characters.'

Verified in Windows Frontier by user testing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude

claude Bot commented Jan 11, 2026

Copy link
Copy Markdown

PR #275 Review: Outline Operation Test Fixes - Phase 4 Alignment

Summary

This PR documents essential outline architecture (empty summit behavior), fixes op.firstSummit() implementation, and aligns test expectations with UserTalk conventions and headless mode behavior. Overall, this is excellent work that significantly improves code quality and documentation.

✅ Strengths

1. Outstanding Documentation

The new docs/OUTLINE_STRUCTURE.md is exceptional:

  • Documents critical architectural fact that all outlines start with empty summit
  • Clear examples showing how this affects op.insert(), op.delete(), line counting
  • Explains cursor visibility invariant (cursor can't be on child of collapsed node)
  • This will save future developers hours of debugging

The planning documents (OUTLINE_EMPTY_SUMMIT_FIX.md, phase4_test_failures.md) provide excellent context for design decisions.

2. Critical Bug Fix: op.firstSummit()

Before: Used opbumpflatup() with infinity, which could fail in edge cases
After: Direct opmoveto((**ho).hsummit) - always returns true

This is the correct fix. The summit is architecturally guaranteed to exist, so direct navigation is both simpler and more reliable than motion-based navigation.

3. Test Expectation Alignment

Correctly identified and fixed test expectations that didn't match UserTalk conventions:

  • Type coercion: UserTalk coerces types (1 → true), tests incorrectly expected strict typing
  • Headless mode: Scroll state is meaningless without visual display - correctly changed to noop
  • Error types: Verified in Windows Frontier that test docs: capture headless daemon vision #21 should expect script_error, not json_parse_error

4. CLAUDE.md Updates

Added two valuable sections:

  • Outline Structure: Quick reference with code examples
  • Memory Logging False Alarms: Explains harmless format detection errors

🔍 Code Quality Assessment

Implementation Changes (tests/headless_op_verbs.c)

op.firstSummit() (lines 303-328):

opmoveto((**ho).hsummit);  // Direct navigation
return setbooleanvalue(true, vreturned);  // Always succeeds

Excellent - Simple, correct, matches architectural guarantee

op.setExpansionState() (lines 940-1018):

  • Uses flinhibitdisplay to disable redraws during batch operations ✅
  • Iterates with opbumpflatdown() through visible nodes ✅
  • Restores cursor to visible ancestor if needed (lines 1005-1011) ✅

Minor concern (line 1006): The cursor restoration loop assumes flexpanded indicates visibility, but the function just SET those flags. Consider whether this needs to check actual expansion state after operations complete.

op.getScrollState() / op.setScrollState() (lines 1019-1061):
Correct - Properly documented as noop in headless mode with clear rationale

op.setCursor() (lines 776-816):

hnode = (hdlheadrecord)nodeid;  // Cast from long
if (!opnodeinoutline(hnode)) {  // Validate before use
    return setbooleanvalue(false, vreturned);
}

Good - Validation prevents using stale/deleted node handles

Test Changes (op_verbs.yaml)

Test #5 (scroll state): Changed expected result from "Line 20" to "Line 10"
Correct - setScrollState is noop in headless, cursor stays at Line 10

Test #11 (cursor independence): Completely rewritten to test expansion state independence
Good - The rewrite makes the test clearer and more focused

Test #21 (error type): Changed from json_parse_error to script_error
Verified - User confirmed this matches Windows Frontier behavior

🚨 Issues & Concerns

1. Potential Memory Safety Issue (MEDIUM PRIORITY)

Location: op.setCursor() at line 798

hnode = (hdlheadrecord)nodeid;  // Unsafe cast from long

Problem: This casts an arbitrary long value to a pointer without validation that it actually points to valid memory. While opnodeinoutline() provides some validation, this could still be exploited:

  1. User calls op.getCursor() → gets handle as long
  2. User modifies/corrupts the value
  3. User calls op.setCursor(corrupted_value) → casts to pointer
  4. opnodeinoutline() might not catch all invalid pointer cases

Recommendation: Consider one of:

  • Store cursor IDs in a validation table (handle → ID mapping)
  • Use a magic number/checksum to validate handles before casting
  • Add more robust pointer validation before opnodeinoutline() call

Risk: Low in trusted code, but problematic if UserTalk scripts come from untrusted sources

2. Test Coverage Gap (LOW PRIORITY)

The PR description states "Integration tests not run yet - will be validated by CI."

Recommendation: Before merge, verify that integration tests pass, especially:

3. Documentation Inconsistency (LOW PRIORITY)

docs/OUTLINE_STRUCTURE.md line 83: "Total lines: 3 (not 2)"

This is correct, but the example at line 72-75 shows:

op.insert("Parent", down);   // Line 2
op.insert("Child", right);   // Line 3

Could be clearer that "Parent" is a summit node (sibling of empty summit), not a child. Consider clarifying:

op.insert("Parent", down);   // Line 2 (new summit, sibling of empty summit)

4. Minor: Expansion State Algorithm Question (LOW PRIORITY)

In op.setExpansionState() (lines 1005-1011), the cursor restoration moves up the tree looking for flexpanded=true:

while (!(**hnode).flexpanded) {
    hnode = (**hnode).headlinkleft;  // Move to parent
}

Question: Since the function just finished setting flexpanded flags, is this guaranteed to terminate? What if the loop walks past the summit? Consider adding a termination check:

while (!(**hnode).flexpanded && hnode != (**ho).hsummit) {
    hnode = (**hnode).headlinkleft;
}

🎯 Performance Considerations

No performance concerns. The changes are well-optimized:

  • op.firstSummit() is now O(1) instead of O(n)
  • Scroll state operations are noop in headless (appropriate)
  • Expansion state uses flinhibitdisplay to batch updates

🔒 Security Assessment

Low risk with one caveat:

✅ No SQL injection, XSS, or similar web vulnerabilities (not applicable)
✅ No obvious buffer overflows (uses handle-based APIs)
⚠️ Pointer casting in op.setCursor() could be hardened (see Issue #1 above)

Recommendation: If UserTalk scripts can come from untrusted sources, add pointer validation layer.

📋 Recommendations

Before Merge (MUST):

  1. ✅ Run integration tests and verify they pass
  2. ✅ Confirm unit tests pass (PR states "All passing")

After Merge (SHOULD):

  1. File issue for cursor ID validation hardening (Issue Repo Modernization: 64‑bit/ARM + DB Header Rev; Headless/Portable Runtime; Headless EFP Routing for file.* and Tests #1 above)
  2. Consider adding test for empty summit deletion workflow
  3. Add test for cursor restoration after setExpansionState() with collapsed parent

Nice to Have:

  1. Minor doc clarification (Issue docs: UTF-8 transition plan #3)
  2. Expansion state loop termination check (Issue planning: relocate UTF-8 plan and add phase directories #4)

✅ Final Verdict

APPROVE with minor recommendations

This PR represents high-quality work:

  • Critical architecture documented for first time ⭐
  • Important bug fix in op.firstSummit()
  • Test expectations aligned with reality ⭐
  • Code follows project conventions
  • Clear commit messages and planning docs

The issues raised above are minor and don't block merge. The pointer casting concern in op.setCursor() should be tracked as a follow-up issue but isn't critical for this PR.

Great job documenting the empty summit behavior - this will save significant debugging time for future developers (including yourself!).


Adherence to CLAUDE.md Guidelines

✅ Follows "Documentation Quick Links" principle - created proper docs
✅ Follows "Technical Decision-Making Principles" - chose proper fix (direct navigation) over workaround
✅ Follows "Knowledge Capture and Documentation" - excellent ADR-style planning docs
✅ No fprintf(stderr) added (follows logging standards)
✅ Type safety aligned with UserTalk conventions (not strict typing)
✅ Tests use project-relative paths (macOS sandbox compatible)

The work demonstrates deep understanding of both the codebase and the project's quality standards.

Issue #1 (Safety): Add pointer validation in op.setCursor()
- Validate nodeid is non-zero before casting to pointer
- Prevents potential null pointer dereference

Issue #2 (Default refcon): Already correctly returns 0
- op.getRefcon() returns 0 when hrefcon is nil (verified)
- No changes needed

Issue #3 (Documentation): Clarify Parent is sibling of empty summit
- Added explanatory comments in OUTLINE_STRUCTURE.md
- Explains op.insert("Parent", down) creates sibling, not child
- Both empty summit and Parent are at summit level

Issue #4 (Loop safety): Add explicit summit check
- Added summit termination check in expansion state restoration
- Checks both self-reference and hsummit comparison
- Prevents potential infinite loop

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude

claude Bot commented Jan 11, 2026

Copy link
Copy Markdown

Code Review - PR #275: Outline Operation Test Fixes

Summary Assessment

APPROVE - This is excellent architectural documentation work that fixes critical test failures and improves codebase understanding.


Strengths

1. Outstanding Documentation

  • docs/OUTLINE_STRUCTURE.md: Exceptional documentation of a critical architectural fact (empty summit behavior)
  • planning/phase3/OUTLINE_EMPTY_SUMMIT_FIX.md: Thorough root cause analysis with clear fix options
  • planning/phase3/phase4_test_failures.md: Methodical test failure categorization with priority assessments

2. Critical Bug Fixes

Issue 1 - Pointer Validation: Prevents null pointer dereference in op.setCursor() (lines 797-801)

Issue 2 - op.firstSummit() Fix: Changed from opmotionkey to direct opmoveto(hsummit), always returns true

Issue 3 - Expansion State Restoration (lines 976-980, 1010-1017):

  • Fixed P0 bug where expansion state wasn't restoring after collapse
  • Root cause: checked (**nomad).flexpanded which gets cleared by opcollapse()
  • Solution: Only check !opsubheadsexpanded() + set flexpanded=true before expanding

Issue 4 - Loop Safety: Proper summit termination check prevents infinite loop (lines 1010-1017)

3. Test Alignment with UserTalk Conventions

  • Correctly identified that many failures were test expectation issues, not bugs
  • Type coercion is CORRECT UserTalk behavior
  • Refcons CAN store any type (not just longs)
  • Scroll state is noop in headless mode

Issues & Recommendations

P1 - Cross-Outline State Validation

Per phase4_test_failures.md, tests #1, #9, #12 expect cross-outline validation to fail. However, legacy Frontier never validated this.

Question: Should cursors/state from different outlines be explicitly rejected, or is opnodeinoutline() sufficient?

P2 - Code Quality

  1. Magic number (line 1044): Consider defining HEADLESS_SCROLL_DEFAULT
  2. Error handling: Standardize on goto cleanup pattern for consistency
  3. Display inhibit: Could benefit from RAII-style guard (lines 963-964, 1020-1021)

P3 - Documentation

  1. Add cursor visibility invariant comment above loop (lines 1010-1017)
  2. Document Test docs: capture headless daemon vision #21 Windows Frontier verification for reproducibility

Security Assessment

✅ No vulnerabilities. Fixes IMPROVE security:

  • Null pointer validation in setCursor
  • Node ownership validation via opnodeinoutline()
  • Infinite loop prevention

Test Coverage

✅ Excellent:


Final Recommendation

APPROVE - Production-ready code with excellent documentation.

Suggested Follow-up (not blocking):

  1. Discuss cross-outline validation strategy
  2. Add max_depth failsafe to cursor visibility loop
  3. Consider display inhibit guard pattern

Merge Confidence: HIGH


🤖 Review by Claude Code (Sonnet 4.5)

@jsavin jsavin merged commit 527dca8 into develop Jan 11, 2026
2 checks passed
@jsavin jsavin deleted the feature/outline-test-fixes branch January 11, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant