@nanobpm/nano-workforce 0.36.0 → 0.37.0
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/.github/workflows/ci.yml +7 -17
- package/AGENTS.md +8 -7
- package/CHANGELOG.md +7 -0
- package/README.md +1 -21
- package/actions/abandon.test.ts +8 -16
- package/actions/blackboard.test.ts +13 -21
- package/app/abandon.test.ts +10 -15
- package/app/baseGuard.test.ts +7 -6
- package/app/blackboard.test.ts +22 -32
- package/app/ensure-pr.test.ts +10 -12
- package/app/github.test.ts +9 -8
- package/app/github.ts +1 -21
- package/app/instance-tracking.test.ts +8 -6
- package/app/mergeExclusion.test.ts +11 -20
- package/app/mergeProtocol.test.ts +14 -13
- package/app/mergeRebaseArm.test.ts +8 -6
- package/app/mergeTrain.test.ts +10 -9
- package/app/persist-escalation.test.ts +9 -30
- package/app/persist-round.test.ts +6 -19
- package/app/plan.test.ts +19 -42
- package/app/record-plan-review.test.ts +8 -7
- package/app/retro.test.ts +24 -48
- package/app/reviewWait.test.ts +12 -11
- package/app/rounds.test.ts +13 -12
- package/app/service.test.ts +14 -27
- package/app/taskDelta.test.ts +8 -17
- package/app/trialMerge.test.ts +4 -3
- package/app/version.ts +6 -21
- package/app/waves.test.ts +17 -16
- package/operations/getVersion.test.ts +8 -12
- package/operations/getVersion.ts +2 -3
- package/operations/listActivePrs.test.ts +8 -14
- package/operations/listActivePrs.ts +2 -3
- package/operations/startAndMessage.test.ts +6 -12
- package/package.json +6 -4
- package/scripts/check-agent-prompts.test.ts +15 -11
- package/scripts/layout-bpmn.ts +6 -28
- package/scripts/pages-contract.test.ts +14 -13
- package/scripts/purge-db.ts +1 -1
- package/scripts/upgrade-from-pack.ts +1 -1
- package/test/assert.ts +74 -0
- package/tsconfig.json +4 -1
- package/workers/record-plan-review/worker.test.ts +6 -9
- package/workers/record-results/worker.test.ts +5 -8
- package/workers/record-trial-merge/worker.test.ts +7 -18
- package/workers/record-wave/worker.test.ts +13 -20
- package/workers/retro-gather/worker.test.ts +4 -17
- package/workers/retro-record/worker.test.ts +8 -27
- package/workers/select-wave/worker.test.ts +5 -8
- package/deno.json +0 -24
- package/deno.lock +0 -1777
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// non-retryable `NO_WORK_DISPATCHED` BpmnError (→ incident) when the epic finalizes with no opened
|
|
7
7
|
// PR, recording a `failed` terminal status + outcome first, so "accomplished nothing" surfaces
|
|
8
8
|
// instead of masquerading as a completed epic.
|
|
9
|
-
import {
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import { assertEquals, assertRejects } from "#test-assert";
|
|
10
11
|
import { BpmnError } from "@nanobpm/urban";
|
|
11
12
|
import handler from "./worker.ts";
|
|
12
13
|
import type { PlanTaskStatus } from "../../app/plan.ts";
|
|
@@ -25,7 +26,6 @@ function fakeApp(rows: Row[]) {
|
|
|
25
26
|
table(name: string, key: string) {
|
|
26
27
|
if (name === "plans") {
|
|
27
28
|
return {
|
|
28
|
-
// deno-lint-ignore no-explicit-any
|
|
29
29
|
update: (k: any, patch: any) => {
|
|
30
30
|
plans.push({ [key]: k, ...patch });
|
|
31
31
|
return Promise.resolve(patch);
|
|
@@ -34,7 +34,6 @@ function fakeApp(rows: Row[]) {
|
|
|
34
34
|
}
|
|
35
35
|
// plan_tasks
|
|
36
36
|
return {
|
|
37
|
-
// deno-lint-ignore no-explicit-any
|
|
38
37
|
find: (q: any) =>
|
|
39
38
|
Promise.resolve(
|
|
40
39
|
rows.filter((r) =>
|
|
@@ -48,15 +47,13 @@ function fakeApp(rows: Row[]) {
|
|
|
48
47
|
},
|
|
49
48
|
log: () => {},
|
|
50
49
|
_plans: plans,
|
|
51
|
-
// deno-lint-ignore no-explicit-any
|
|
52
50
|
} as any;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
const call = async (app: unknown, planKey = "o/r#1") =>
|
|
56
|
-
// deno-lint-ignore no-explicit-any
|
|
57
54
|
await handler({ variables: { planKey } } as any, app as any);
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
test("no opened PRs (empty plan) hard-fails with NO_WORK_DISPATCHED", async () => {
|
|
60
57
|
const app = fakeApp([]);
|
|
61
58
|
const err = await assertRejects(() => call(app), BpmnError);
|
|
62
59
|
assertEquals((err as BpmnError).errorCode, "NO_WORK_DISPATCHED");
|
|
@@ -67,7 +64,7 @@ Deno.test("no opened PRs (empty plan) hard-fails with NO_WORK_DISPATCHED", async
|
|
|
67
64
|
assertEquals(plan.outcome, "no work dispatched — the planner produced no tasks");
|
|
68
65
|
});
|
|
69
66
|
|
|
70
|
-
|
|
67
|
+
test("tasks present but none opened (all skipped/blocked) hard-fails", async () => {
|
|
71
68
|
const app = fakeApp([
|
|
72
69
|
{ id: 1, plan_key: "o/r#1", task_id: "a", status: "skipped" },
|
|
73
70
|
{ id: 2, plan_key: "o/r#1", task_id: "b", status: "blocked" },
|
|
@@ -79,7 +76,7 @@ Deno.test("tasks present but none opened (all skipped/blocked) hard-fails", asyn
|
|
|
79
76
|
assertEquals(plan.outcome, "no work dispatched — every task was blocked or skipped");
|
|
80
77
|
});
|
|
81
78
|
|
|
82
|
-
|
|
79
|
+
test("at least one opened PR finalizes cleanly (no throw)", async () => {
|
|
83
80
|
const app = fakeApp([
|
|
84
81
|
{ id: 1, plan_key: "o/r#1", task_id: "a", status: "opened" },
|
|
85
82
|
{ id: 2, plan_key: "o/r#1", task_id: "b", status: "skipped" },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { assertEquals, assertStringIncludes } from "#test-assert";
|
|
2
3
|
import handler, { parseResult } from "./worker.ts";
|
|
3
4
|
|
|
4
5
|
function fakeApp() {
|
|
@@ -7,16 +8,13 @@ function fakeApp() {
|
|
|
7
8
|
data: {
|
|
8
9
|
table(name: string, _key: string) {
|
|
9
10
|
return {
|
|
10
|
-
// deno-lint-ignore require-await
|
|
11
11
|
async find(_q: unknown) {
|
|
12
12
|
return [];
|
|
13
13
|
},
|
|
14
|
-
// deno-lint-ignore require-await
|
|
15
14
|
async insert(row: unknown) {
|
|
16
15
|
(inserts[name] ??= []).push(row);
|
|
17
16
|
return 7;
|
|
18
17
|
},
|
|
19
|
-
// deno-lint-ignore require-await
|
|
20
18
|
async update(_key: unknown, _patch: unknown) {},
|
|
21
19
|
};
|
|
22
20
|
},
|
|
@@ -26,31 +24,27 @@ function fakeApp() {
|
|
|
26
24
|
return { app, inserts };
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
test("record-trial-merge shapes clean results as proceed", async () => {
|
|
30
28
|
const { app, inserts } = fakeApp();
|
|
31
29
|
const out = await handler(
|
|
32
|
-
// deno-lint-ignore no-explicit-any
|
|
33
30
|
{ key: 11, variables: { planKey: "o/r#69", currentWave: 2, result: "clean", summary: "green" } } as any,
|
|
34
|
-
// deno-lint-ignore no-explicit-any
|
|
35
31
|
app as any,
|
|
36
32
|
) as Record<string, unknown>;
|
|
37
33
|
|
|
38
34
|
assertEquals(out.trialMergeRed, false);
|
|
39
35
|
assertEquals(inserts.plan_trial_merges.length, 1);
|
|
40
|
-
// deno-lint-ignore no-explicit-any
|
|
41
36
|
assertEquals((inserts.plan_trial_merges[0] as any).result, "clean");
|
|
42
37
|
});
|
|
43
38
|
|
|
44
|
-
|
|
39
|
+
test("parseResult trims valid agent result strings", () => {
|
|
45
40
|
assertEquals(parseResult("clean\n"), "clean");
|
|
46
41
|
assertEquals(parseResult(" merge-conflict "), "merge-conflict");
|
|
47
42
|
assertEquals(parseResult("unknown"), "suite-failed");
|
|
48
43
|
});
|
|
49
44
|
|
|
50
|
-
|
|
45
|
+
test("record-trial-merge escalates only suite-failed results", async () => {
|
|
51
46
|
const { app } = fakeApp();
|
|
52
47
|
const out = await handler(
|
|
53
|
-
// deno-lint-ignore no-explicit-any
|
|
54
48
|
{
|
|
55
49
|
key: 12,
|
|
56
50
|
variables: {
|
|
@@ -61,7 +55,6 @@ Deno.test("record-trial-merge escalates only suite-failed results", async () =>
|
|
|
61
55
|
failing: ["integration suite"],
|
|
62
56
|
},
|
|
63
57
|
} as any,
|
|
64
|
-
// deno-lint-ignore no-explicit-any
|
|
65
58
|
app as any,
|
|
66
59
|
) as Record<string, unknown>;
|
|
67
60
|
|
|
@@ -71,10 +64,9 @@ Deno.test("record-trial-merge escalates only suite-failed results", async () =>
|
|
|
71
64
|
assertStringIncludes(String(out.question ?? ""), "proceed");
|
|
72
65
|
});
|
|
73
66
|
|
|
74
|
-
|
|
67
|
+
test("record-trial-merge renders odd failing payloads without throwing", async () => {
|
|
75
68
|
const { app } = fakeApp();
|
|
76
69
|
const out = await handler(
|
|
77
|
-
// deno-lint-ignore no-explicit-any
|
|
78
70
|
{
|
|
79
71
|
variables: {
|
|
80
72
|
planKey: "o/r#69",
|
|
@@ -83,7 +75,6 @@ Deno.test("record-trial-merge renders odd failing payloads without throwing", as
|
|
|
83
75
|
failing: [1n],
|
|
84
76
|
},
|
|
85
77
|
} as any,
|
|
86
|
-
// deno-lint-ignore no-explicit-any
|
|
87
78
|
app as any,
|
|
88
79
|
) as Record<string, unknown>;
|
|
89
80
|
|
|
@@ -91,12 +82,10 @@ Deno.test("record-trial-merge renders odd failing payloads without throwing", as
|
|
|
91
82
|
assertStringIncludes(String(out.question ?? ""), "Failing:");
|
|
92
83
|
});
|
|
93
84
|
|
|
94
|
-
|
|
85
|
+
test("record-trial-merge treats textual conflicts as pass-through", async () => {
|
|
95
86
|
const { app } = fakeApp();
|
|
96
87
|
const out = await handler(
|
|
97
|
-
// deno-lint-ignore no-explicit-any
|
|
98
88
|
{ variables: { planKey: "o/r#69", result: "merge-conflict", conflicts: [{ prs: [1, 2] }] } } as any,
|
|
99
|
-
// deno-lint-ignore no-explicit-any
|
|
100
89
|
app as any,
|
|
101
90
|
) as Record<string, unknown>;
|
|
102
91
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Red/green regression for retrying a wave when `select-wave` deliberately left pending work
|
|
2
2
|
// behind a non-fatal wait (D7 / issue #63). Advancing past that wave would make `record-results`
|
|
3
3
|
// finish the plan with a still-pending task.
|
|
4
|
-
import {
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { assertEquals } from "#test-assert";
|
|
5
6
|
import handler from "./worker.ts";
|
|
6
7
|
import type { PlanTaskStatus } from "../../app/plan.ts";
|
|
7
8
|
import { _clearMergeProtocolCache } from "../../app/mergeProtocol.ts";
|
|
@@ -32,10 +33,8 @@ function fakeApp(rows: Row[]) {
|
|
|
32
33
|
table(name: string, key: string) {
|
|
33
34
|
const store = stores[name] ??= [];
|
|
34
35
|
return {
|
|
35
|
-
// deno-lint-ignore no-explicit-any
|
|
36
36
|
get: (k: any) =>
|
|
37
37
|
Promise.resolve(store.find((r) => ((r as unknown) as Record<string, unknown>)[key] === k)),
|
|
38
|
-
// deno-lint-ignore no-explicit-any
|
|
39
38
|
find: (q: any) =>
|
|
40
39
|
Promise.resolve(
|
|
41
40
|
store.filter((r) =>
|
|
@@ -44,12 +43,10 @@ function fakeApp(rows: Row[]) {
|
|
|
44
43
|
)
|
|
45
44
|
),
|
|
46
45
|
),
|
|
47
|
-
// deno-lint-ignore no-explicit-any
|
|
48
46
|
insert: (row: any) => {
|
|
49
47
|
store.push(row);
|
|
50
48
|
return Promise.resolve(store.length);
|
|
51
49
|
},
|
|
52
|
-
// deno-lint-ignore no-explicit-any
|
|
53
50
|
update: (k: any, patch: any) => {
|
|
54
51
|
if (name === "plans") {
|
|
55
52
|
planUpdates.push({ key: k, patch });
|
|
@@ -68,7 +65,6 @@ function fakeApp(rows: Row[]) {
|
|
|
68
65
|
engine: {
|
|
69
66
|
createInstance: () => Promise.resolve({ processInstanceKey: "pi" }),
|
|
70
67
|
},
|
|
71
|
-
// deno-lint-ignore no-explicit-any
|
|
72
68
|
} as any,
|
|
73
69
|
planUpdates,
|
|
74
70
|
};
|
|
@@ -76,11 +72,11 @@ function fakeApp(rows: Row[]) {
|
|
|
76
72
|
|
|
77
73
|
function installGithubStub(method: "gh-merge" | "mergify-queue") {
|
|
78
74
|
_clearMergeProtocolCache();
|
|
79
|
-
const oldTransport =
|
|
80
|
-
const oldToken =
|
|
75
|
+
const oldTransport = process.env["NANO_PR_GITHUB_TRANSPORT"];
|
|
76
|
+
const oldToken = process.env["GITHUB_TOKEN"];
|
|
81
77
|
const oldFetch = globalThis.fetch;
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
process.env["NANO_PR_GITHUB_TRANSPORT"] = "token";
|
|
79
|
+
process.env["GITHUB_TOKEN"] = "test-token";
|
|
84
80
|
globalThis.fetch = ((input: string | URL | Request) => {
|
|
85
81
|
const url = String(input);
|
|
86
82
|
if (url.includes("/contents/AGENTS.md")) {
|
|
@@ -104,16 +100,16 @@ function installGithubStub(method: "gh-merge" | "mergify-queue") {
|
|
|
104
100
|
return Promise.resolve(new Response("not found", { status: 404 }));
|
|
105
101
|
}) as typeof fetch;
|
|
106
102
|
return () => {
|
|
107
|
-
if (oldTransport == null)
|
|
108
|
-
else
|
|
109
|
-
if (oldToken == null)
|
|
110
|
-
else
|
|
103
|
+
if (oldTransport == null) delete process.env["NANO_PR_GITHUB_TRANSPORT"];
|
|
104
|
+
else process.env["NANO_PR_GITHUB_TRANSPORT"] = oldTransport;
|
|
105
|
+
if (oldToken == null) delete process.env["GITHUB_TOKEN"];
|
|
106
|
+
else process.env["GITHUB_TOKEN"] = oldToken;
|
|
111
107
|
globalThis.fetch = oldFetch;
|
|
112
108
|
_clearMergeProtocolCache();
|
|
113
109
|
};
|
|
114
110
|
}
|
|
115
111
|
|
|
116
|
-
|
|
112
|
+
test("record-wave retries the same wave when a task is still pending", async () => {
|
|
117
113
|
const rows: Row[] = [{
|
|
118
114
|
id: 2,
|
|
119
115
|
plan_key: "owner/repo#63",
|
|
@@ -124,7 +120,6 @@ Deno.test("record-wave retries the same wave when a task is still pending", asyn
|
|
|
124
120
|
const { app, planUpdates } = fakeApp(rows);
|
|
125
121
|
|
|
126
122
|
const out = await handler(
|
|
127
|
-
// deno-lint-ignore no-explicit-any
|
|
128
123
|
{
|
|
129
124
|
variables: {
|
|
130
125
|
planKey: "owner/repo#63",
|
|
@@ -148,7 +143,7 @@ Deno.test("record-wave retries the same wave when a task is still pending", asyn
|
|
|
148
143
|
assertEquals((planUpdates[0].patch as Record<string, unknown>).gate_wave, 1);
|
|
149
144
|
});
|
|
150
145
|
|
|
151
|
-
|
|
146
|
+
test("record-wave skips trial merge for mergify-queue repos with 2+ heads", async () => {
|
|
152
147
|
const restore = installGithubStub("mergify-queue");
|
|
153
148
|
try {
|
|
154
149
|
const rows: Row[] = [
|
|
@@ -157,7 +152,6 @@ Deno.test("record-wave skips trial merge for mergify-queue repos with 2+ heads",
|
|
|
157
152
|
];
|
|
158
153
|
const { app } = fakeApp(rows);
|
|
159
154
|
const out = await handler(
|
|
160
|
-
// deno-lint-ignore no-explicit-any
|
|
161
155
|
{
|
|
162
156
|
variables: {
|
|
163
157
|
planKey: "owner/repo#69",
|
|
@@ -184,7 +178,7 @@ Deno.test("record-wave skips trial merge for mergify-queue repos with 2+ heads",
|
|
|
184
178
|
}
|
|
185
179
|
});
|
|
186
180
|
|
|
187
|
-
|
|
181
|
+
test("record-wave runs trial merge for non-queue repos with populated heads", async () => {
|
|
188
182
|
const restore = installGithubStub("gh-merge");
|
|
189
183
|
try {
|
|
190
184
|
const rows: Row[] = [
|
|
@@ -193,7 +187,6 @@ Deno.test("record-wave runs trial merge for non-queue repos with populated heads
|
|
|
193
187
|
];
|
|
194
188
|
const { app } = fakeApp(rows);
|
|
195
189
|
const out = await handler(
|
|
196
|
-
// deno-lint-ignore no-explicit-any
|
|
197
190
|
{
|
|
198
191
|
variables: {
|
|
199
192
|
planKey: "owner/repo#69",
|
|
@@ -1,47 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { assert, assertEquals, assertStringIncludes } from "#test-assert";
|
|
2
3
|
import type { DataLayer } from "@nanobpm/urban";
|
|
3
4
|
import { appendEntry } from "../../app/blackboard.ts";
|
|
4
5
|
import handler from "./worker.ts";
|
|
5
6
|
|
|
6
|
-
// deno-lint-ignore no-explicit-any
|
|
7
7
|
function memData(): { data: DataLayer; stores: Record<string, any[]> } {
|
|
8
|
-
// deno-lint-ignore no-explicit-any
|
|
9
8
|
const stores: Record<string, any[]> = {};
|
|
10
9
|
const seq: Record<string, number> = {};
|
|
11
10
|
function tbl(name: string, pk = "id") {
|
|
12
|
-
// deno-lint-ignore no-explicit-any
|
|
13
11
|
const rows = (stores[name] ??= [] as any[]);
|
|
14
|
-
// deno-lint-ignore no-explicit-any
|
|
15
12
|
const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
|
|
16
13
|
return {
|
|
17
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
18
14
|
async insert(row: any) {
|
|
19
15
|
const id = (seq[name] = (seq[name] ?? 0) + 1);
|
|
20
16
|
rows.push(pk === "id" ? { id, ...row } : { ...row });
|
|
21
17
|
return pk === "id" ? id : row[pk];
|
|
22
18
|
},
|
|
23
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
24
19
|
async find(where: any = {}) {
|
|
25
20
|
return rows.filter((r) => match(r, where));
|
|
26
21
|
},
|
|
27
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
28
22
|
async findOne(where: any = {}) {
|
|
29
23
|
return rows.find((r) => match(r, where));
|
|
30
24
|
},
|
|
31
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
32
25
|
async get(id: any) {
|
|
33
26
|
return rows.find((row) => row[pk] === id);
|
|
34
27
|
},
|
|
35
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
36
28
|
async update() {},
|
|
37
29
|
};
|
|
38
30
|
}
|
|
39
|
-
// deno-lint-ignore no-explicit-any
|
|
40
31
|
const data = { table: (n: string, pk?: string) => tbl(n, pk) } as any as DataLayer;
|
|
41
32
|
return { data, stores };
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
|
|
35
|
+
test("retro-gather: emits a digest brief + learning count for the plan", async () => {
|
|
45
36
|
const { data, stores } = memData();
|
|
46
37
|
stores["plans"] = [{ plan_key: "o/r#3", repo: "o/r", issue_url: "https://x/3", title: "Epic" }];
|
|
47
38
|
await appendEntry(data, "o/r#3", { author_task: "t1", kind: "learning", body: "regen before build" });
|
|
@@ -49,9 +40,7 @@ Deno.test("retro-gather: emits a digest brief + learning count for the plan", as
|
|
|
49
40
|
|
|
50
41
|
const app = { data, log: () => undefined };
|
|
51
42
|
const out = await handler(
|
|
52
|
-
// deno-lint-ignore no-explicit-any
|
|
53
43
|
{ variables: { planKey: "o/r#3" } } as any,
|
|
54
|
-
// deno-lint-ignore no-explicit-any
|
|
55
44
|
app as any,
|
|
56
45
|
) as Record<string, unknown>;
|
|
57
46
|
|
|
@@ -61,14 +50,12 @@ Deno.test("retro-gather: emits a digest brief + learning count for the plan", as
|
|
|
61
50
|
assertStringIncludes(String(out.retroDigest), "o/r#3");
|
|
62
51
|
});
|
|
63
52
|
|
|
64
|
-
|
|
53
|
+
test("retro-gather: an epic with no learnings still renders a valid brief", async () => {
|
|
65
54
|
const { data, stores } = memData();
|
|
66
55
|
stores["plans"] = [{ plan_key: "o/r#4", repo: "o/r", issue_url: "", title: null }];
|
|
67
56
|
const app = { data, log: () => undefined };
|
|
68
57
|
const out = await handler(
|
|
69
|
-
// deno-lint-ignore no-explicit-any
|
|
70
58
|
{ variables: { planKey: "o/r#4" } } as any,
|
|
71
|
-
// deno-lint-ignore no-explicit-any
|
|
72
59
|
app as any,
|
|
73
60
|
) as Record<string, unknown>;
|
|
74
61
|
|
|
@@ -1,35 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { assertEquals } from "#test-assert";
|
|
2
3
|
import handler from "./worker.ts";
|
|
3
4
|
|
|
4
5
|
function fakeApp() {
|
|
5
|
-
// deno-lint-ignore no-explicit-any
|
|
6
6
|
const stores: Record<string, any[]> = { plan_retros: [] };
|
|
7
7
|
const seq: Record<string, number> = {};
|
|
8
8
|
function tbl(name: string, pk = "id") {
|
|
9
|
-
// deno-lint-ignore no-explicit-any
|
|
10
9
|
const rows = (stores[name] ??= [] as any[]);
|
|
11
|
-
// deno-lint-ignore no-explicit-any
|
|
12
10
|
const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
|
|
13
11
|
return {
|
|
14
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
15
12
|
async insert(row: any) {
|
|
16
13
|
const id = (seq[name] = (seq[name] ?? 0) + 1);
|
|
17
14
|
rows.push(pk === "id" ? { id, ...row } : { ...row });
|
|
18
15
|
return pk === "id" ? id : row[pk];
|
|
19
16
|
},
|
|
20
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
21
17
|
async find(where: any = {}) {
|
|
22
18
|
return rows.filter((r) => match(r, where));
|
|
23
19
|
},
|
|
24
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
25
20
|
async findOne(where: any = {}) {
|
|
26
21
|
return rows.find((r) => match(r, where));
|
|
27
22
|
},
|
|
28
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
29
23
|
async get(id: any) {
|
|
30
24
|
return rows.find((row) => row[pk] === id);
|
|
31
25
|
},
|
|
32
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
33
26
|
async update(id: any, patch: any) {
|
|
34
27
|
const r = rows.find((row) => row[pk] === id);
|
|
35
28
|
if (r) Object.assign(r, patch);
|
|
@@ -40,10 +33,9 @@ function fakeApp() {
|
|
|
40
33
|
return { app, stores };
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
test("retro-record: persists a filed retro from hoisted result vars", async () => {
|
|
44
37
|
const { app, stores } = fakeApp();
|
|
45
38
|
await handler(
|
|
46
|
-
// deno-lint-ignore no-explicit-any
|
|
47
39
|
{
|
|
48
40
|
variables: {
|
|
49
41
|
planKey: "o/r#5",
|
|
@@ -54,7 +46,6 @@ Deno.test("retro-record: persists a filed retro from hoisted result vars", async
|
|
|
54
46
|
"io.nanobpm.agentResult": { output: "the full report" },
|
|
55
47
|
},
|
|
56
48
|
} as any,
|
|
57
|
-
// deno-lint-ignore no-explicit-any
|
|
58
49
|
app as any,
|
|
59
50
|
);
|
|
60
51
|
|
|
@@ -67,59 +58,49 @@ Deno.test("retro-record: persists a filed retro from hoisted result vars", async
|
|
|
67
58
|
assertEquals(row.report, "the full report");
|
|
68
59
|
});
|
|
69
60
|
|
|
70
|
-
|
|
61
|
+
test("retro-record: defaults to skipped when the agent filed no PR", async () => {
|
|
71
62
|
const { app, stores } = fakeApp();
|
|
72
63
|
await handler(
|
|
73
|
-
// deno-lint-ignore no-explicit-any
|
|
74
64
|
{ variables: { planKey: "o/r#6", summary: "nothing durable" } } as any,
|
|
75
|
-
// deno-lint-ignore no-explicit-any
|
|
76
65
|
app as any,
|
|
77
66
|
);
|
|
78
67
|
assertEquals(stores.plan_retros[0].status, "skipped");
|
|
79
68
|
assertEquals(stores.plan_retros[0].pr_key, null);
|
|
80
69
|
});
|
|
81
70
|
|
|
82
|
-
|
|
71
|
+
test("retro-record: honours an explicit blocked status", async () => {
|
|
83
72
|
const { app, stores } = fakeApp();
|
|
84
73
|
await handler(
|
|
85
|
-
// deno-lint-ignore no-explicit-any
|
|
86
74
|
{ variables: { planKey: "o/r#7", status: "blocked", summary: "no write access" } } as any,
|
|
87
|
-
// deno-lint-ignore no-explicit-any
|
|
88
75
|
app as any,
|
|
89
76
|
);
|
|
90
77
|
assertEquals(stores.plan_retros[0].status, "blocked");
|
|
91
78
|
});
|
|
92
79
|
|
|
93
|
-
|
|
80
|
+
test("retro-record: ignores a PR unless the status is filed", async () => {
|
|
94
81
|
const { app, stores } = fakeApp();
|
|
95
82
|
await handler(
|
|
96
|
-
// deno-lint-ignore no-explicit-any
|
|
97
83
|
{ variables: { planKey: "o/r#8", status: "skipped", pr: "o/r#43", summary: "not durable" } } as any,
|
|
98
|
-
// deno-lint-ignore no-explicit-any
|
|
99
84
|
app as any,
|
|
100
85
|
);
|
|
101
86
|
assertEquals(stores.plan_retros[0].status, "skipped");
|
|
102
87
|
assertEquals(stores.plan_retros[0].pr_key, null);
|
|
103
88
|
});
|
|
104
89
|
|
|
105
|
-
|
|
90
|
+
test("retro-record: defaults invalid status from PR presence", async () => {
|
|
106
91
|
const { app, stores } = fakeApp();
|
|
107
92
|
await handler(
|
|
108
|
-
// deno-lint-ignore no-explicit-any
|
|
109
93
|
{ variables: { planKey: "o/r#9", status: "done", pr: "o/r#44" } } as any,
|
|
110
|
-
// deno-lint-ignore no-explicit-any
|
|
111
94
|
app as any,
|
|
112
95
|
);
|
|
113
96
|
assertEquals(stores.plan_retros[0].status, "filed");
|
|
114
97
|
assertEquals(stores.plan_retros[0].pr_key, "o/r#44");
|
|
115
98
|
});
|
|
116
99
|
|
|
117
|
-
|
|
100
|
+
test("retro-record: coerces filed without a PR to skipped", async () => {
|
|
118
101
|
const { app, stores } = fakeApp();
|
|
119
102
|
await handler(
|
|
120
|
-
// deno-lint-ignore no-explicit-any
|
|
121
103
|
{ variables: { planKey: "o/r#10", status: "filed", summary: "forgot the PR" } } as any,
|
|
122
|
-
// deno-lint-ignore no-explicit-any
|
|
123
104
|
app as any,
|
|
124
105
|
);
|
|
125
106
|
assertEquals(stores.plan_retros[0].status, "skipped");
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// A predecessor parked behind a merge lane is not a failed slice: dependents must remain pending
|
|
4
4
|
// so a later retry can dispatch them once the lane clears. Real non-open failures still cascade
|
|
5
5
|
// to `skipped` as before.
|
|
6
|
-
import {
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { assertEquals } from "#test-assert";
|
|
7
8
|
import handler from "./worker.ts";
|
|
8
9
|
import type { PlanTaskStatus } from "../../app/plan.ts";
|
|
9
10
|
|
|
@@ -30,7 +31,6 @@ function fakeApp(rows: Row[], deps: DepRow[]) {
|
|
|
30
31
|
table(name: string, key: string) {
|
|
31
32
|
const store = name === "plan_tasks" ? rows : deps;
|
|
32
33
|
return {
|
|
33
|
-
// deno-lint-ignore no-explicit-any
|
|
34
34
|
find: (q: any) =>
|
|
35
35
|
Promise.resolve(
|
|
36
36
|
store.filter((r) =>
|
|
@@ -39,7 +39,6 @@ function fakeApp(rows: Row[], deps: DepRow[]) {
|
|
|
39
39
|
)
|
|
40
40
|
),
|
|
41
41
|
),
|
|
42
|
-
// deno-lint-ignore no-explicit-any
|
|
43
42
|
update: (k: any, patch: any) => {
|
|
44
43
|
const row = store.find((r) =>
|
|
45
44
|
((r as unknown) as Record<string, unknown>)[key] === k
|
|
@@ -50,20 +49,18 @@ function fakeApp(rows: Row[], deps: DepRow[]) {
|
|
|
50
49
|
};
|
|
51
50
|
},
|
|
52
51
|
},
|
|
53
|
-
// deno-lint-ignore no-explicit-any
|
|
54
52
|
} as any;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
async function selectWave(rows: Row[], deps: DepRow[]) {
|
|
58
56
|
const out = await handler(
|
|
59
|
-
// deno-lint-ignore no-explicit-any
|
|
60
57
|
{ variables: { planKey: "owner/repo#63", currentWave: 1 } } as any,
|
|
61
58
|
fakeApp(rows, deps),
|
|
62
59
|
);
|
|
63
60
|
return out as { waveTasks: unknown[] };
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
test("select-wave leaves dependents pending behind a waiting-for-lane dependency", async () => {
|
|
67
64
|
const rows: Row[] = [
|
|
68
65
|
{
|
|
69
66
|
id: 1,
|
|
@@ -95,9 +92,9 @@ Deno.test("select-wave leaves dependents pending behind a waiting-for-lane depen
|
|
|
95
92
|
assertEquals(rows[1].summary, undefined);
|
|
96
93
|
});
|
|
97
94
|
|
|
98
|
-
|
|
95
|
+
test("select-wave still skips dependents behind failed or otherwise non-open dependencies", async (t) => {
|
|
99
96
|
for (const depStatus of ["blocked", "skipped", "pending"] as const) {
|
|
100
|
-
await t.
|
|
97
|
+
await t.test(depStatus, async () => {
|
|
101
98
|
const rows: Row[] = [
|
|
102
99
|
{
|
|
103
100
|
id: 1,
|
package/deno.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"lib": [
|
|
4
|
-
"deno.window",
|
|
5
|
-
"deno.ns"
|
|
6
|
-
]
|
|
7
|
-
},
|
|
8
|
-
"imports": {
|
|
9
|
-
"@nanobpm/urban": "npm:@nanobpm/urban@^0.33.0"
|
|
10
|
-
},
|
|
11
|
-
"tasks": {
|
|
12
|
-
"start": "deno run --allow-net --allow-read --allow-write --allow-run=gh --allow-env main.ts",
|
|
13
|
-
"purge": "deno run --allow-read --allow-write --allow-env scripts/purge-db.ts",
|
|
14
|
-
"check": "deno run -A @nanobpm/urban check",
|
|
15
|
-
"check:prompts": "deno run -A scripts/check-agent-prompts.ts",
|
|
16
|
-
"gen": "deno run -A @nanobpm/urban gen",
|
|
17
|
-
"gen:check": "deno run -A @nanobpm/urban gen --check",
|
|
18
|
-
"layout": "deno run -A scripts/layout-bpmn.ts",
|
|
19
|
-
"layout:check": "deno run -A scripts/layout-bpmn.ts --check",
|
|
20
|
-
"dev": "deno run -A @nanobpm/urban dev",
|
|
21
|
-
"compile": "deno compile --allow-net --allow-read --allow-write --allow-run=gh --allow-env -o dist/nano-workforce main.ts",
|
|
22
|
-
"test": "deno test -A"
|
|
23
|
-
}
|
|
24
|
-
}
|