@nanobpm/nano-workforce 0.53.0 → 0.55.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/CHANGELOG.md +14 -0
- package/app/agentic/cockpit/index.ts +30 -0
- package/app/agentic/cockpit/supply-boot.test.ts +279 -0
- package/app/agentic/cockpit/supply-boot.ts +282 -0
- package/app/agentic/cockpit/supply-render.test.ts +76 -0
- package/app/agentic/cockpit/supply-render.ts +144 -0
- package/app/agentic/cockpit/supply-view.test.ts +76 -0
- package/app/agentic/cockpit/supply-view.ts +165 -0
- package/app/agentic/families/blackboard.family.test.ts +189 -0
- package/app/agentic/families/blackboard.family.ts +69 -0
- package/app/blackboard.schema.test.ts +21 -0
- package/app/blackboard.test.ts +37 -69
- package/app/blackboard.ts +101 -124
- package/app/retro.test.ts +3 -1
- package/db/migrations/025_agentic_blackboard.sql +39 -0
- package/openapi.yaml +94 -0
- package/operations/blackboard.test.ts +11 -27
- package/operations/getAgenticSupply.test.ts +153 -0
- package/operations/getAgenticSupply.ts +59 -0
- package/package.json +1 -1
- package/pages/cockpit/cockpit.css +145 -0
- package/pages/cockpit/embed.html +42 -0
- package/pages/cockpit/mount.js +331 -0
- package/pages/cockpit/standalone.html +46 -0
- package/pages/cockpit.page.json +46 -0
- package/pages/epic-detail.page.json +2 -1
- package/pages/epic.page.json +2 -1
- package/pages/home.page.json +4 -0
- package/test/agentic-cockpit-doubles.ts +136 -0
- package/test/blackboardDb.ts +108 -0
- package/workers/retro-gather/worker.test.ts +2 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// A test-only DataLayer stub backed by a real in-memory `node:sqlite` database, for exercising the
|
|
2
|
+
// blackboard adapter (`app/blackboard.ts`) and the agentic `blackboard` family against a real SQLite
|
|
3
|
+
// engine rather than a mock. It mirrors the two surfaces the adapter uses:
|
|
4
|
+
// - `data.source().db` — the raw synchronous `SqliteDb` the shared `BlackboardStore` writes to,
|
|
5
|
+
// - `data.table(name)` — the async record gateway (only the `plans` table is needed here, for
|
|
6
|
+
// token→plan resolution), backed by the SAME db so the sync (`planKeyForTokenSync`) and async
|
|
7
|
+
// (`planKeyForToken`) paths see identical rows.
|
|
8
|
+
import { DatabaseSync, type SQLInputValue } from "node:sqlite";
|
|
9
|
+
import { afterEach } from "node:test";
|
|
10
|
+
import type { DataLayer } from "@nanobpm/urban";
|
|
11
|
+
|
|
12
|
+
/** The tiny synchronous SQLite handle shape the runtime + the agentic store share. */
|
|
13
|
+
interface SqliteDb {
|
|
14
|
+
exec(sql: string): void;
|
|
15
|
+
run(sql: string, params?: unknown[]): { changes: number; lastInsertRowid: number | bigint };
|
|
16
|
+
all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
|
|
17
|
+
close(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function coerce(p: unknown): SQLInputValue {
|
|
21
|
+
if (p === null) return null;
|
|
22
|
+
if (typeof p === "boolean") return p ? 1 : 0;
|
|
23
|
+
return p as SQLInputValue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wrap(db: DatabaseSync): SqliteDb {
|
|
27
|
+
return {
|
|
28
|
+
exec: (sql) => db.exec(sql),
|
|
29
|
+
run: (sql, params = []) => {
|
|
30
|
+
const r = db.prepare(sql).run(...params.map(coerce));
|
|
31
|
+
return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };
|
|
32
|
+
},
|
|
33
|
+
all: <T>(sql: string, params: unknown[] = []) => db.prepare(sql).all(...params.map(coerce)) as T[],
|
|
34
|
+
close: () => db.close(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Every raw handle these helpers open is tracked here and released after each test, so call sites
|
|
39
|
+
// that drop the returned `close()` (most of them) don't leak native SQLite handles across the run.
|
|
40
|
+
const openDbs = new Set<DatabaseSync>();
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
for (const raw of openDbs) closeTracked(raw);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** Open a tracked in-memory db and return it with an idempotent `close()` safe to call twice. */
|
|
47
|
+
function openTracked(): { raw: DatabaseSync; close(): void } {
|
|
48
|
+
const raw = new DatabaseSync(":memory:");
|
|
49
|
+
openDbs.add(raw);
|
|
50
|
+
return { raw, close: () => closeTracked(raw) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function closeTracked(raw: DatabaseSync): void {
|
|
54
|
+
if (openDbs.delete(raw)) raw.close();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A minimal async record gateway over the real db — just the insert/find/findOne subset the
|
|
58
|
+
* blackboard tests exercise on the `plans` table. */
|
|
59
|
+
function gateway(db: SqliteDb, name: string, pk: string) {
|
|
60
|
+
const quote = (id: string) => `"${id.replace(/"/g, '""')}"`;
|
|
61
|
+
return {
|
|
62
|
+
// biome-ignore lint/suspicious/noExplicitAny: test-only gateway over dynamic row shapes.
|
|
63
|
+
async insert(row: any): Promise<number | bigint | unknown> {
|
|
64
|
+
const keys = Object.keys(row).filter((k) => row[k] !== undefined);
|
|
65
|
+
const cols = keys.map(quote).join(", ");
|
|
66
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
67
|
+
const r = db.run(
|
|
68
|
+
`INSERT INTO ${quote(name)} (${cols}) VALUES (${placeholders})`,
|
|
69
|
+
keys.map((k) => row[k]),
|
|
70
|
+
);
|
|
71
|
+
return pk === "id" ? r.lastInsertRowid : row[pk];
|
|
72
|
+
},
|
|
73
|
+
// biome-ignore lint/suspicious/noExplicitAny: test-only gateway over dynamic row shapes.
|
|
74
|
+
async find(where: any = {}): Promise<any[]> {
|
|
75
|
+
const keys = Object.keys(where);
|
|
76
|
+
const clause = keys.length ? `WHERE ${keys.map((k) => `${quote(k)} = ?`).join(" AND ")}` : "";
|
|
77
|
+
return db.all(`SELECT * FROM ${quote(name)} ${clause}`, keys.map((k) => where[k]));
|
|
78
|
+
},
|
|
79
|
+
// biome-ignore lint/suspicious/noExplicitAny: test-only gateway over dynamic row shapes.
|
|
80
|
+
async findOne(where: any = {}): Promise<any> {
|
|
81
|
+
return (await this.find(where))[0];
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A DataLayer stub over a fresh in-memory SQLite db, plus a `plans` table for token resolution. */
|
|
87
|
+
export function memBlackboardData(): { data: DataLayer; db: SqliteDb; close(): void } {
|
|
88
|
+
const { raw, close } = openTracked();
|
|
89
|
+
const db = wrap(raw);
|
|
90
|
+
db.exec("CREATE TABLE IF NOT EXISTS plans (plan_key TEXT PRIMARY KEY, blackboard_token TEXT);");
|
|
91
|
+
const data = {
|
|
92
|
+
source: () => ({ db }),
|
|
93
|
+
table: (name: string, pk = "id") => gateway(db, name, pk),
|
|
94
|
+
} as unknown as DataLayer;
|
|
95
|
+
return { data, db, close };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A bare real-sqlite handle (no tables) for tests whose fake DataLayer keeps its OTHER tables as
|
|
100
|
+
* in-memory arrays but still needs the blackboard's `data.source().db` seam to resolve to a real
|
|
101
|
+
* SQLite engine (the store applies its own schema via `ensureSchema()`). Spread its `.source` into
|
|
102
|
+
* the fake `data`: `{ ...fake, source: bb.source }`.
|
|
103
|
+
*/
|
|
104
|
+
export function memBlackboardSource(): { source: () => { db: SqliteDb }; db: SqliteDb; close(): void } {
|
|
105
|
+
const { raw, close } = openTracked();
|
|
106
|
+
const db = wrap(raw);
|
|
107
|
+
return { source: () => ({ db }), db, close };
|
|
108
|
+
}
|
|
@@ -2,6 +2,7 @@ import { test } from "node:test";
|
|
|
2
2
|
import { assert, assertEquals, assertStringIncludes } from "#test-assert";
|
|
3
3
|
import type { DataLayer } from "@nanobpm/urban";
|
|
4
4
|
import { noopLog } from "../../test/log.ts";
|
|
5
|
+
import { memBlackboardSource } from "../../test/blackboardDb.ts";
|
|
5
6
|
import { appendEntry } from "../../app/blackboard.ts";
|
|
6
7
|
import handler from "./worker.ts";
|
|
7
8
|
|
|
@@ -29,7 +30,7 @@ function memData(): { data: DataLayer; stores: Record<string, any[]> } {
|
|
|
29
30
|
async update() {},
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
|
-
const data = { table: (n: string, pk?: string) => tbl(n, pk) } as any as DataLayer;
|
|
33
|
+
const data = { table: (n: string, pk?: string) => tbl(n, pk), source: memBlackboardSource().source } as any as DataLayer;
|
|
33
34
|
return { data, stores };
|
|
34
35
|
}
|
|
35
36
|
|