diff --git a/.github/trigger-deploy b/.github/trigger-deploy index 2b6b599c..5aef4763 100644 --- a/.github/trigger-deploy +++ b/.github/trigger-deploy @@ -2,3 +2,4 @@ 2026-07-17T16:15:00Z deploy openapi-fallback crawl 2026-07-17T17:20:00Z deploy manifest-merge + alias collapse 2026-07-17T18:00:00Z deploy price tiebreak +2026-07-18T02:00:00Z deploy wish-bridge qualified-demand gate diff --git a/.github/trigger-test b/.github/trigger-test index 1f1d47f9..778e78bf 100644 --- a/.github/trigger-test +++ b/.github/trigger-test @@ -13,3 +13,4 @@ 2026-07-17T17:05:00Z manifest-path bazaar merge + router alias collapse 2026-07-17T17:45:00Z price tiebreak: known beats unknown 2026-07-18T01:40:00Z wish bridge: qualified-demand gate against single-source bursts +2026-07-18T02:30:00Z operator private demand board diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index df49a95a..651e6737 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1013,6 +1013,9 @@ jobs: - name: Sell hub page tests (offline, fixture snapshot) run: node scripts/test-sell-page.js + - name: Operator demand-board page tests (verdict rendering, no double-escape, robots disallow — offline) + run: node scripts/test-operator-wishes.js + - name: Index self-serve registration tests (offline) run: node scripts/test-index-register.js diff --git a/scripts/test-operator-wishes.js b/scripts/test-operator-wishes.js new file mode 100644 index 00000000..f087ec2e --- /dev/null +++ b/scripts/test-operator-wishes.js @@ -0,0 +1,67 @@ +// Offline unit tests for the token-gated operator demand board +// (src/operator-wishes.js) and its robots.txt disallow. Pure rendering, no +// server boot: the route-level 404 gate is exercised by the manual/boot path; +// here we assert the page reflects the qualification verdicts correctly and +// that the operator surface is kept out of robots. +import { operatorWishesPage } from "../src/operator-wishes.js"; +import { robotsTxt } from "../src/seo.js"; + +let pass = 0, fail = 0; +const ok = (c, m) => { if (c) { pass++; console.log(`ok - ${m}`); } else { fail++; console.error(`FAIL - ${m}`); } }; + +const BASE = "https://agent402.tools"; +const aggregate = { + distinctClusters: 4, + totalWishes: 130, + threshold: 5, + qualifyMinSpanHours: 24, + clusters: [ + // single-source burst — at threshold but must read as held, not qualified + { text: "synthora mesh 962 m2m", count: 103, qualified: false, + sources: { api: 103, mcp: 0, "find-miss": 0 }, firstSeen: "2026-07-17T02:00:00.000Z", lastSeen: "2026-07-17T06:00:00.000Z" }, + // genuine multi-source demand — qualified + { text: "convert heic to png batch", count: 6, qualified: true, + sources: { api: 2, mcp: 2, "find-miss": 2 }, firstSeen: "2026-07-15T10:00:00.000Z", lastSeen: "2026-07-16T10:00:00.000Z" }, + // below threshold + { text: "solidity auditor", count: 2, qualified: false, + sources: { api: 0, mcp: 0, "find-miss": 2 }, firstSeen: "2026-07-13T10:00:00.000Z", lastSeen: "2026-07-13T10:05:00.000Z" }, + // an entity with HTML-significant chars already esc()'d by getWishesAggregate + { text: "handle <script> safely", count: 1, qualified: false, + sources: { api: 1, mcp: 0, "find-miss": 0 }, firstSeen: "2026-07-16T00:00:00.000Z", lastSeen: "2026-07-16T00:00:00.000Z" }, + ], +}; + +const html = operatorWishesPage(BASE, aggregate); + +// --- summary reflects the split --- +ok(/Distinct clusters/.test(html) && html.includes(">4<"), "summary shows distinct cluster count"); +ok(html.includes("Qualified") && html.includes(">1<"), "summary shows 1 qualified"); +ok(/Held/.test(html), "summary shows the held (spam/near-miss) bucket"); + +// --- per-cluster verdicts --- +ok(/qualified<\/span>/.test(html), "a qualified cluster renders the qualified badge"); +ok(/single-source<\/span>/.test(html), "the single-source burst renders as single-source, not qualified"); +ok(/below<\/span>/.test(html), "the sub-threshold cluster renders as below"); +ok(html.includes("one surface, not corroborated"), "single-source note explains why it is held"); + +// --- the operator can see everything, including unqualified/below rows --- +ok(html.includes("synthora mesh 962 m2m"), "single-source spam is still visible to the operator"); +ok(html.includes("convert heic to png batch"), "qualified demand is visible"); +ok(html.includes("solidity auditor"), "below-threshold demand is visible"); + +// --- already-escaped text is not double-escaped --- +ok(html.includes("<script>") && !html.includes(" +${ledgerFooterCompact()}`; + + return ledgerShell({ + title: "Operator · Agent demand — Agent402", + description: "Agent402 operator dashboard — full agent demand board.", + canonical: `${baseUrl}/__operator/wishes`, + baseUrl, + activePath: "__none__", + extraCss, + body, + }); +} diff --git a/src/operator.js b/src/operator.js index 72f2e066..290d5128 100644 --- a/src/operator.js +++ b/src/operator.js @@ -76,7 +76,7 @@ td a:hover{color:var(--accent)}
Per-tool usage, settlement split, and live activity. Auto-refreshes every 10s. Not public — gated by AGENT402_OPERATOR_TOKEN. Tollbooth leads
Per-tool usage, settlement split, and live activity. Auto-refreshes every 10s. Not public — gated by AGENT402_OPERATOR_TOKEN. Agent demand · Tollbooth leads
Not found.
"); + res.type("html").send(operatorWishesPage(BASE_URL, getWishesAggregate({ limit: 500 }))); +}); app.get("/__operator/leads", async (req, res) => { if (!operatorTokenOk(getOperatorToken(req))) return res.status(404).type("html").send("Not found.
"); const list = await listLeads({ limit: 200 });