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
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def _load_workflow_functions():
f.write(func_source)
f.write(
"\n\nmodule.exports = "
"{ parseMachineState, parseSchedule, getProgramName, readProgramState };\n"
"{ parseMachineState, parseSchedule, getProgramName, readProgramState, parseLinkHeader };\n"
)

return True
Expand Down Expand Up @@ -102,6 +102,7 @@ def _get_program_name_wrapper(pf):
"parse_machine_state": _parse_machine_state_wrapper,
"get_program_name": _get_program_name_wrapper,
"read_program_state": lambda name: _call_js("readProgramState", name),
"parse_link_header": lambda header: _call_js("parseLinkHeader", header),
}


Expand Down
33 changes: 33 additions & 0 deletions tests/test_scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
parse_schedule = _funcs["parse_schedule"]
parse_machine_state = _funcs["parse_machine_state"]
get_program_name = _funcs["get_program_name"]
parse_link_header = _funcs["parse_link_header"]


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -663,6 +664,35 @@ def test_forced_program_target_metric_fallback_via_frontmatter(self):
assert target == 0.95


# ---------------------------------------------------------------------------
# parseLinkHeader — extract next-page URL from GitHub API Link header
# ---------------------------------------------------------------------------

class TestParseLinkHeader:
def test_returns_null_for_none(self):
assert parse_link_header(None) is None

def test_returns_null_for_empty_string(self):
assert parse_link_header("") is None

def test_extracts_next_url(self):
header = '<https://api.github.com/repos/o/r/issues?page=2&per_page=100>; rel="next", <https://api.github.com/repos/o/r/issues?page=5&per_page=100>; rel="last"'
assert parse_link_header(header) == "https://api.github.com/repos/o/r/issues?page=2&per_page=100"

def test_returns_null_when_no_next(self):
header = '<https://api.github.com/repos/o/r/issues?page=1&per_page=100>; rel="prev", <https://api.github.com/repos/o/r/issues?page=5&per_page=100>; rel="last"'
assert parse_link_header(header) is None

def test_next_not_first(self):
"""next rel is not the first segment."""
header = '<https://api.github.com/repos/o/r/issues?page=1&per_page=100>; rel="prev", <https://api.github.com/repos/o/r/issues?page=3&per_page=100>; rel="next", <https://api.github.com/repos/o/r/issues?page=5&per_page=100>; rel="last"'
assert parse_link_header(header) == "https://api.github.com/repos/o/r/issues?page=3&per_page=100"

def test_single_next_segment(self):
header = '<https://api.github.com/repos/o/r/issues?page=2&per_page=100>; rel="next"'
assert parse_link_header(header) == "https://api.github.com/repos/o/r/issues?page=2&per_page=100"


# ---------------------------------------------------------------------------
# Extraction sanity check — verify conftest.py found the expected functions
# ---------------------------------------------------------------------------
Expand All @@ -677,6 +707,9 @@ def test_parse_machine_state_extracted(self):
def test_get_program_name_extracted(self):
assert callable(get_program_name)

def test_parse_link_header_extracted(self):
assert callable(parse_link_header)

def test_read_program_state_extracted(self):
# read_program_state exists in the workflow but depends on file I/O
assert "read_program_state" in _funcs
36 changes: 27 additions & 9 deletions workflows/autoloop.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ steps:
}
}

// Parse the GitHub API Link header to extract the "next" page URL.
// Returns the URL string for the next page, or null if there is none.
function parseLinkHeader(header) {
if (!header) return null;
var parts = header.split(',');
for (var i = 0; i < parts.length; i++) {
var section = parts[i].trim();
var m = section.match(/^<([^>]+)>;\s*rel="next"$/);
if (m) return m[1];
}
return null;
}

// Main execution
async function main() {
// Bootstrap: create autoloop programs directory and template if missing
Expand Down Expand Up @@ -273,18 +286,23 @@ steps:
} catch (e) { /* stat failed */ }
}

// Scan GitHub issues with the 'autoloop-program' label
// Scan GitHub issues with the 'autoloop-program' label (paginated)
const issueProgramsDir = '/tmp/gh-aw/issue-programs';
fs.mkdirSync(issueProgramsDir, { recursive: true });
try {
const apiUrl = 'https://api.github.com/repos/' + repo + '/issues?labels=autoloop-program&state=open&per_page=100';
const response = await fetch(apiUrl, {
headers: {
'Authorization': 'token ' + githubToken,
'Accept': 'application/vnd.github.v3+json',
},
});
const issues = await response.json();
let nextUrl = 'https://api.github.com/repos/' + repo + '/issues?labels=autoloop-program&state=open&per_page=100';
const issues = [];
while (nextUrl) {
const response = await fetch(nextUrl, {
headers: {
'Authorization': 'token ' + githubToken,
'Accept': 'application/vnd.github.v3+json',
},
});
const page = await response.json();
issues.push(...page);
nextUrl = parseLinkHeader(response.headers.get('link'));
}
for (const issue of issues) {
if (issue.pull_request) continue; // skip PRs
const body = issue.body || '';
Expand Down
Loading