@nanobpm/nano-workforce 0.189.1 → 0.189.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.189.2](https://github.com/nanobpm/nano-workforce/compare/v0.189.1...v0.189.2) (2026-09-18)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **poller:** fetch newest GitHub reviews ([#797](https://github.com/nanobpm/nano-workforce/issues/797)) ([c488410](https://github.com/nanobpm/nano-workforce/commit/c488410645d1df55ca5121448364c8f89ed4a7a6)), closes [#793](https://github.com/nanobpm/nano-workforce/issues/793)
6
+
1
7
  ## [0.189.1](https://github.com/nanobpm/nano-workforce/compare/v0.189.0...v0.189.1) (2026-09-17)
2
8
 
3
9
  ### Bug Fixes
@@ -3,7 +3,7 @@
3
3
  // the merge-exclusion graph. Force the token transport and stub `globalThis.fetch`.
4
4
  import { test } from "node:test";
5
5
  import { assertEquals, assertRejects } from "#test-assert";
6
- import { BaseBranchMustExistError, checkConclusions, classifyMergeability, classifyPrLiveness, coalesceTitle, createPullRequest, ensureBaseBranch, ensurePromotionPr, fetchBranchHead, fetchIssueTitle, fetchPrFiles, fetchPrHead, fetchPrReviews, isNotAPullRequestError, listPrsForHead, type Mergeability, type PrState } from "./github.ts";
6
+ import { BaseBranchMustExistError, checkConclusions, classifyMergeability, classifyPrLiveness, coalesceTitle, createPullRequest, ensureBaseBranch, ensurePromotionPr, fetchBranchHead, fetchIssueTitle, fetchPrFiles, fetchPrHead, fetchPrReviews, isNotAPullRequestError, listPrsForHead, type GhReview, type Mergeability, type PrState } from "./github.ts";
7
7
  import { DEFAULT_MERGE_PROTOCOL, type MergeProtocol, type RequiredCheck } from "./mergeProtocol.ts";
8
8
 
9
9
  // A fake `fetch` that serves `pages` of file batches; each page N (1-based) returns `pages[N-1]`
@@ -40,6 +40,51 @@ async function withTokenTransport<T>(pages: number[], fn: () => Promise<T>): Pro
40
40
  }
41
41
  }
42
42
 
43
+ function reviewFetch(pages: GhReview[][], requests: string[]) {
44
+ return (url: string | URL | Request): Promise<Response> => {
45
+ const u = new URL(String(url));
46
+ requests.push(u.toString());
47
+ const page = Number(u.searchParams.get("page") ?? "1");
48
+ const headers = new Headers();
49
+ if (page < pages.length) {
50
+ headers.set(
51
+ "link",
52
+ `<https://api.github.com/repos/o/r/pulls/1/reviews?per_page=100&page=${page + 1}>; rel="next", ` +
53
+ `<https://api.github.com/repos/o/r/pulls/1/reviews?per_page=100&page=${pages.length}>; rel="last"`,
54
+ );
55
+ }
56
+ return Promise.resolve(new Response(JSON.stringify(pages[page - 1] ?? []), { status: 200, headers }));
57
+ };
58
+ }
59
+
60
+ async function withTokenFetch<T>(fetchImpl: typeof fetch, fn: () => Promise<T>): Promise<T> {
61
+ const prevMode = process.env["NANO_PR_GITHUB_TRANSPORT"];
62
+ const prevFetch = globalThis.fetch;
63
+ process.env["NANO_PR_GITHUB_TRANSPORT"] = "token";
64
+ globalThis.fetch = fetchImpl;
65
+ try {
66
+ return await fn();
67
+ } finally {
68
+ globalThis.fetch = prevFetch;
69
+ if (prevMode === undefined) delete process.env["NANO_PR_GITHUB_TRANSPORT"];
70
+ else process.env["NANO_PR_GITHUB_TRANSPORT"] = prevMode;
71
+ }
72
+ }
73
+
74
+ test("fetchPrReviews: returns the newest review when it is beyond page one", async () => {
75
+ const requests: string[] = [];
76
+ const pages = [
77
+ Array.from({ length: 100 }, (_, i) => ({ id: i + 1, state: "COMMENTED" })),
78
+ [{ id: 101, state: "APPROVED", submitted_at: "2026-09-15T12:00:00Z" }],
79
+ ];
80
+ const reviews = await withTokenFetch(reviewFetch(pages, requests) as typeof fetch, () =>
81
+ fetchPrReviews("o/r", 1, "tok"),
82
+ );
83
+ assertEquals(reviews?.length, 101);
84
+ assertEquals(reviews?.[reviews.length - 1]?.id, 101);
85
+ assertEquals(requests.length, 2, "the final page must be fetched after page one");
86
+ });
87
+
43
88
  test("fetchPrFiles: returns the complete list for a sub-cap PR (short final page)", async () => {
44
89
  const files = await withTokenTransport([100, 42], () => fetchPrFiles("o/r", 1, "tok"));
45
90
  assertEquals(files?.length, 142);
@@ -11,9 +11,10 @@ import { memDataFor } from "../test/worldDb.ts";
11
11
  import { withTrackingViews } from "../test/trackingViews.ts";
12
12
  import { DurableResumeRegistry } from "./durableResume.ts";
13
13
  import { WorldStore } from "./world/index.ts";
14
- import { abandonClosedPr, isPrSettled, MAX_ACK_RETRIES, parsePr, pollCapabilityGatesImpl, pollIncidentsImpl, pollWaveGatesImpl, repoEnvelopeVars, startMerge, submitPr, worldRestoreSha } from "./service.ts";
14
+ import { abandonClosedPr, isPrSettled, MAX_ACK_RETRIES, parsePr, pollCapabilityGatesImpl, pollIncidentsImpl, pollReviews, pollWaveGatesImpl, repoEnvelopeVars, startMerge, submitPr, worldRestoreSha } from "./service.ts";
15
15
  import { trackingTargetFor } from "./instanceTracking.ts";
16
16
  import type { DataLayer } from "@nanobpm/urban";
17
+ import { READINESS_READY_MESSAGE } from "./readiness.ts";
17
18
 
18
19
  function memTable(rows: any[], key: string) {
19
20
  return {
@@ -78,6 +79,90 @@ function withGithubOff(run: () => Promise<void>): Promise<void> {
78
79
  });
79
80
  }
80
81
 
82
+ function reviewPagesFetch(pages: Record<string, unknown>[][], requests: string[]) {
83
+ return (url: string | URL | Request): Promise<Response> => {
84
+ const u = new URL(String(url));
85
+ if (!u.pathname.endsWith("/reviews")) {
86
+ return Promise.resolve(
87
+ new Response(
88
+ JSON.stringify({ head: { ref: null, sha: "SHA_CURRENT" } }),
89
+ { status: 200, headers: { "content-type": "application/json" } },
90
+ ),
91
+ );
92
+ }
93
+ requests.push(u.toString());
94
+ const page = Number(u.searchParams.get("page") ?? "1");
95
+ const headers = new Headers();
96
+ if (page < pages.length) {
97
+ headers.set(
98
+ "link",
99
+ `<https://api.github.com/repos/owner/repo/pulls/42/reviews?per_page=100&page=${page + 1}>; rel="next", ` +
100
+ `<https://api.github.com/repos/owner/repo/pulls/42/reviews?per_page=100&page=${pages.length}>; rel="last"`,
101
+ );
102
+ }
103
+ return Promise.resolve(new Response(JSON.stringify(pages[page - 1] ?? []), { status: 200, headers }));
104
+ };
105
+ }
106
+
107
+ test("pollReviews publishes readiness-ready for a fresh review on the final page (#793)", async () => {
108
+ const oldReviews = Array.from({ length: 100 }, (_, i) => ({
109
+ id: i + 1,
110
+ state: "COMMENTED",
111
+ submitted_at: "2026-09-01T00:00:00Z",
112
+ commit_id: "SHA_CURRENT",
113
+ }));
114
+ const pages = [
115
+ oldReviews,
116
+ [{ id: 101, state: "APPROVED", submitted_at: "2026-09-15T12:00:00Z", commit_id: "SHA_CURRENT" }],
117
+ ];
118
+ const requests: string[] = [];
119
+ const pr = {
120
+ pr_key: "owner/repo#42",
121
+ repo: "owner/repo",
122
+ number: 42,
123
+ status: "waiting_review",
124
+ waiting_since: "2026-09-10T00:00:00Z",
125
+ last_review_id: 100,
126
+ };
127
+ const stores: Record<string, { rows: any[]; key: string }> = {
128
+ pull_requests: { rows: [pr], key: "pr_key" },
129
+ };
130
+ const data = {
131
+ table: (name: string, key: string) => memTable(stores[name]?.rows ?? [], key),
132
+ } as any as DataLayer;
133
+ const messages: { name: string; correlationKey?: string; variables?: Record<string, unknown> }[] = [];
134
+ const engine = {
135
+ publishMessage: async (message: { name: string; correlationKey?: string; variables?: Record<string, unknown> }) => {
136
+ messages.push(message);
137
+ },
138
+ } as any;
139
+
140
+ const prevMode = process.env["NANO_PR_GITHUB_TRANSPORT"];
141
+ const prevFetch = globalThis.fetch;
142
+ process.env["NANO_PR_GITHUB_TRANSPORT"] = "token";
143
+ globalThis.fetch = reviewPagesFetch(pages, requests) as typeof fetch;
144
+ try {
145
+ await pollReviews(data, engine, "tok");
146
+ } finally {
147
+ globalThis.fetch = prevFetch;
148
+ if (prevMode === undefined) delete process.env["NANO_PR_GITHUB_TRANSPORT"];
149
+ else process.env["NANO_PR_GITHUB_TRANSPORT"] = prevMode;
150
+ }
151
+
152
+ assertEquals(requests.length, 2, "the poller must read the final reviews page");
153
+ assertEquals(
154
+ messages,
155
+ [{
156
+ name: READINESS_READY_MESSAGE,
157
+ correlationKey: "owner/repo#42",
158
+ variables: { ready: true, detail: "review 101 (APPROVED)" },
159
+ }],
160
+ "fresh review must release the review wait",
161
+ );
162
+ assertEquals(pr.last_review_id, 101);
163
+ assertEquals(pr.status, "converging");
164
+ });
165
+
81
166
  test("isPrSettled reads the derived tracking view — an out-of-band-abandoned PR (base row still converging) is settled", async () => {
82
167
  // The base `pull_requests` row still reads `converging`, but the ADR-0065 derived tracking VIEW
83
168
  // folds the reconciler's out-of-band terminal edge into `derived_status: "abandoned"`. Terminal-edge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.189.1",
3
+ "version": "0.189.2",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",