@bongos/core 1.25.12 → 1.25.13
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/.bongos-core.json +65 -30
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +11 -2
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/api/openapi.json +192 -11
- package/docs/api-reference.md +5 -2
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/front-door-guide/claude.js +11 -7
- package/modules/front-door-guide/context.js +54 -0
- package/modules/front-door-guide/db.js +71 -6
- package/modules/front-door-guide/guide.js +10 -1
- package/modules/front-door-guide/migrations/front_door_guide_002_plan_and_account.sql +37 -0
- package/modules/front-door-guide/module.json +7 -3
- package/modules/front-door-guide/plan.js +93 -0
- package/modules/front-door-guide/pollers/retention-sweep.js +67 -0
- package/modules/front-door-guide/routes/front-door-guide-plan.js +144 -0
- package/modules/front-door-guide/routes/front-door-guide.js +19 -29
- package/modules/front-door-guide/settings.js +4 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/seams.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +1 -1
- package/tests/front_door_guide_db.mjs +110 -0
- package/tests/front_door_guide_plan.mjs +344 -0
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
// POST /front-door-guide/conversations/:id/messages send one message { token, text, website? }
|
|
8
8
|
// GET /front-door-guide/conversations/:id read it back header x-guide-token
|
|
9
9
|
//
|
|
10
|
+
// The plan, attaching a chat to an account at sign-up, and "my plan" are in
|
|
11
|
+
// routes/front-door-guide-plan.js (task 1005273).
|
|
12
|
+
//
|
|
10
13
|
// rank: public — every route. The visitor has no account yet; that is the point. What
|
|
11
14
|
// stands in for one: per-visitor rate limits, a bot check, a token per conversation, and
|
|
12
15
|
// every money cap enforced here before a call is made (the per-visitor message count and
|
|
@@ -25,37 +28,16 @@ const db = require('../db');
|
|
|
25
28
|
const claude = require('../claude');
|
|
26
29
|
const { readSettings } = require('../settings');
|
|
27
30
|
const L = require('../limits');
|
|
31
|
+
const C = require('../context');
|
|
32
|
+
const P = require('../plan');
|
|
28
33
|
|
|
29
34
|
const log = api.logger('front-door-guide');
|
|
30
35
|
const ID_RE = /^\d{1,18}$/;
|
|
31
36
|
const TOKEN_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
32
37
|
|
|
33
|
-
function salt() {
|
|
34
|
-
let s;
|
|
35
|
-
try { s = api.resolveEnv('FRONT_DOOR_GUIDE_SALT'); } catch (_) { s = null; }
|
|
36
|
-
if (s) return s;
|
|
37
|
-
let name = 'bongos';
|
|
38
|
-
try { name = api.instanceDbName() || name; } catch (_) { /* the default salt still separates visitors */ }
|
|
39
|
-
return `front-door-guide:${name}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function productName() {
|
|
43
|
-
try { return api.branding().identity.productName || 'Cloud Bongos'; } catch (_) { return 'Cloud Bongos'; }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Can the site afford a call of `extraMicro` right now? → null, or the form reason.
|
|
47
|
-
async function siteBudgetReason(settings, extraMicro, reservations) {
|
|
48
|
-
const pending = reservations.siteMicro();
|
|
49
|
-
// Spent is spent (>=), and a call must also fit in what is left (>).
|
|
50
|
-
const over = (spent, cap) => spent + pending >= cap || spent + pending + extraMicro > cap;
|
|
51
|
-
if (over(await db.siteSpendMicro('day'), settings.dailyBudgetMicro)) return 'daily_budget';
|
|
52
|
-
if (over(await db.siteSpendMicro('month'), settings.monthlyBudgetMicro)) return 'monthly_budget';
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
38
|
module.exports = function buildFrontDoorGuideRouter() {
|
|
57
39
|
const router = express.Router();
|
|
58
|
-
const reservations =
|
|
40
|
+
const { reservations, siteBudgetReason, salt, productName } = C;
|
|
59
41
|
let current = readSettings();
|
|
60
42
|
const knobs = () => current;
|
|
61
43
|
|
|
@@ -68,7 +50,7 @@ module.exports = function buildFrontDoorGuideRouter() {
|
|
|
68
50
|
// rank: public — the landing asks before it offers the chat.
|
|
69
51
|
router.get('/front-door-guide/status', fresh, statusLimit, api.asyncHandler('GET /front-door-guide/status', async (req, res) => {
|
|
70
52
|
const s = knobs();
|
|
71
|
-
const reason = !s.apiKey ? 'no_key' : await siteBudgetReason(s, 0
|
|
53
|
+
const reason = !s.apiKey ? 'no_key' : await siteBudgetReason(s, 0);
|
|
72
54
|
res.json({
|
|
73
55
|
ok: true,
|
|
74
56
|
mode: reason ? 'form' : 'chat',
|
|
@@ -86,7 +68,7 @@ module.exports = function buildFrontDoorGuideRouter() {
|
|
|
86
68
|
return res.json(G.formAnswer('check_failed'));
|
|
87
69
|
}
|
|
88
70
|
if (!s.apiKey) return res.json(G.formAnswer('no_key'));
|
|
89
|
-
const budget = await siteBudgetReason(s, 0
|
|
71
|
+
const budget = await siteBudgetReason(s, 0);
|
|
90
72
|
if (budget) return res.json(G.formAnswer(budget));
|
|
91
73
|
const visitor = L.visitorHash(L.clientIp(req), salt());
|
|
92
74
|
const used = await db.visitorUsage(visitor);
|
|
@@ -97,6 +79,7 @@ module.exports = function buildFrontDoorGuideRouter() {
|
|
|
97
79
|
ok: true, mode: 'chat',
|
|
98
80
|
conversation: { ...G.conversationView(conv, s), token },
|
|
99
81
|
greeting: G.GREETING,
|
|
82
|
+
saved: G.savedNotice(s.retentionDays),
|
|
100
83
|
});
|
|
101
84
|
}));
|
|
102
85
|
|
|
@@ -122,7 +105,10 @@ module.exports = function buildFrontDoorGuideRouter() {
|
|
|
122
105
|
const conv = await db.getConversation(req.params.id, L.tokenHash(body.token));
|
|
123
106
|
if (!conv) return res.status(404).json({ ok: false, error: 'not_found' });
|
|
124
107
|
const view = () => G.conversationView(conv, s);
|
|
125
|
-
const ended = (reason) => ({
|
|
108
|
+
const ended = (reason) => ({
|
|
109
|
+
ok: true, mode: 'ended', reason, message: G.END_REASONS[reason] || G.END_REASONS.message_cap,
|
|
110
|
+
next_step: reason === 'planned' ? 'sign_up' : 'plan', conversation: view(),
|
|
111
|
+
});
|
|
126
112
|
if (conv.status === 'ended') return res.json(ended(conv.end_reason));
|
|
127
113
|
|
|
128
114
|
const bot = G.botCheck({
|
|
@@ -148,7 +134,7 @@ module.exports = function buildFrontDoorGuideRouter() {
|
|
|
148
134
|
await db.endConversation(conv.id, 'visitor_budget');
|
|
149
135
|
return res.json(ended('visitor_budget'));
|
|
150
136
|
}
|
|
151
|
-
const budget = await siteBudgetReason(s, estimate
|
|
137
|
+
const budget = await siteBudgetReason(s, estimate);
|
|
152
138
|
if (budget) return res.json(G.formAnswer(budget));
|
|
153
139
|
|
|
154
140
|
const release = reservations.take(visitor, estimate);
|
|
@@ -182,7 +168,7 @@ module.exports = function buildFrontDoorGuideRouter() {
|
|
|
182
168
|
res.json({
|
|
183
169
|
ok: true,
|
|
184
170
|
mode: endReason ? 'ended' : 'chat',
|
|
185
|
-
...(endReason ? { reason: endReason, message: G.END_REASONS[endReason] } : {}),
|
|
171
|
+
...(endReason ? { reason: endReason, message: G.END_REASONS[endReason], next_step: 'plan' } : {}),
|
|
186
172
|
...(refused ? { declined: true } : {}),
|
|
187
173
|
reply: { text: reply },
|
|
188
174
|
conversation: G.conversationView(saved || { ...conv, visitor_messages: Number(conv.visitor_messages) + 1 }, s),
|
|
@@ -199,6 +185,10 @@ module.exports = function buildFrontDoorGuideRouter() {
|
|
|
199
185
|
ok: true,
|
|
200
186
|
conversation: { ...G.conversationView(conv, knobs()), status: conv.status, end_reason: conv.end_reason || null },
|
|
201
187
|
transcript: G.transcriptView(conv.messages),
|
|
188
|
+
plan: conv.plan || null,
|
|
189
|
+
...(conv.plan ? { next: P.NEXT_STEP } : {}),
|
|
190
|
+
saved: G.savedNotice(knobs().retentionDays),
|
|
191
|
+
attached: conv.builder_id != null,
|
|
202
192
|
});
|
|
203
193
|
}));
|
|
204
194
|
|
|
@@ -25,6 +25,8 @@ const DEFAULTS = Object.freeze({
|
|
|
25
25
|
STARTS_PER_HOUR: 5, // new conversations per visitor per hour
|
|
26
26
|
MESSAGES_PER_MINUTE: 6, // messages per visitor per minute
|
|
27
27
|
MIN_FIRST_MESSAGE_MS: 1500, // a person takes longer than this to type a first line
|
|
28
|
+
MAX_PLAN_TOKENS: 4000, // the plan, thinking included
|
|
29
|
+
RETENTION_DAYS: 30, // a chat nobody signed up for is deleted after this
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
@@ -62,6 +64,8 @@ function readSettings({ resolve = defaultResolve } = {}) {
|
|
|
62
64
|
startsPerHour: intKnob(read, 'STARTS_PER_HOUR'),
|
|
63
65
|
messagesPerMinute: intKnob(read, 'MESSAGES_PER_MINUTE'),
|
|
64
66
|
minFirstMessageMs: intKnob(read, 'MIN_FIRST_MESSAGE_MS'),
|
|
67
|
+
maxPlanTokens: intKnob(read, 'MAX_PLAN_TOKENS'),
|
|
68
|
+
retentionDays: intKnob(read, 'RETENTION_DAYS'),
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -7,6 +7,7 @@ const api = require('../../src/module-api');
|
|
|
7
7
|
const { pool } = api;
|
|
8
8
|
const provisioning = require('./provisioning');
|
|
9
9
|
const { selfUpdateChannel } = require('./core-upgrade');
|
|
10
|
+
const { ALWAYS_ON_CORE, optionalModules } = require('./starter-bundles');
|
|
10
11
|
|
|
11
12
|
// The `provisioning.projectPublishIdentity` port (task 1003578, ADR 0247 D4): the name and
|
|
12
13
|
// description the publish gate judges, for the control-plane project a hub catalog row was
|
|
@@ -44,6 +45,11 @@ async function projectPublishIdentity(db, { slug, host } = {}) {
|
|
|
44
45
|
// question names both halves of the ADR 0246 projection.
|
|
45
46
|
// • PROVIDES port `provisioning.projectPublishIdentity` ({ slug, host }) → { name,
|
|
46
47
|
// description } | null — see projectPublishIdentity above.
|
|
48
|
+
// • PROVIDES port `provisioning.moduleCatalog` () → { core: [key], optional: [{ key,
|
|
49
|
+
// label, summary }] } — the modules a NEW project gets and may opt into, with the
|
|
50
|
+
// owner-facing words the creation picker shows (task 1005273). The start-a-project
|
|
51
|
+
// guide recommends only from this list, so its plan can never name a module the
|
|
52
|
+
// picker does not offer. Read-only; no database.
|
|
47
53
|
function registerProvisioningSeams() {
|
|
48
54
|
if (api.hasProvider('provisioning.instanceState')) return;
|
|
49
55
|
api.registerProvider('provisioning.instanceState', (slug) => provisioning.instanceStateForSlug(pool, slug));
|
|
@@ -51,6 +57,10 @@ function registerProvisioningSeams() {
|
|
|
51
57
|
api.registerProvider('provisioning.projectOwnedBy', (target) => provisioning.projectOwnedBy(pool, target));
|
|
52
58
|
api.registerProvider('provisioning.projectPublishIdentity', (target) => projectPublishIdentity(pool, target));
|
|
53
59
|
api.registerProvider('provisioning.selfUpdateChannel', () => selfUpdateChannel(pool));
|
|
60
|
+
api.registerProvider('provisioning.moduleCatalog', () => ({
|
|
61
|
+
core: [...ALWAYS_ON_CORE],
|
|
62
|
+
optional: optionalModules().map(({ key, label, summary }) => ({ key, label, summary })),
|
|
63
|
+
}));
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
module.exports = { registerProvisioningSeams, projectPublishIdentity };
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.13",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.13",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.13",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11782,5 +11782,11 @@
|
|
|
11782
11782
|
"id": "1005288",
|
|
11783
11783
|
"text": "Decided how every project gets a sandbox: a free copy of the app that always shows the newest finished work, while the real site only changes when a person presses promote. Fake data and test keys by default. Costs the owner n"
|
|
11784
11784
|
}
|
|
11785
|
+
],
|
|
11786
|
+
"1.25.13": [
|
|
11787
|
+
{
|
|
11788
|
+
"id": "1005273",
|
|
11789
|
+
"text": "The project guide now ends with a written plan (with recommended features drawn only from what Bongos really offers), saves the chat so visitors can come back to it, moves it onto their account when they sign up, and deletes i"
|
|
11790
|
+
}
|
|
11785
11791
|
]
|
|
11786
11792
|
}
|
|
@@ -100,6 +100,10 @@ const INTEGRATION = new Set([
|
|
|
100
100
|
// task 1004941: a rules proposal round-trips to a new version the weekly run's loader
|
|
101
101
|
// reads, on real SQL (the one-active index, the CHECKs). DB-free sibling: market_watch_rules.
|
|
102
102
|
'market_watch_rules_db',
|
|
103
|
+
// tasks 1005272/1005273: the start-a-project guide's SQL on a real engine: the migrations,
|
|
104
|
+
// the one-account attach rule, and the retention sweep keeping spend on the books.
|
|
105
|
+
// DB-free siblings: front_door_guide, front_door_guide_plan.
|
|
106
|
+
'front_door_guide_db',
|
|
103
107
|
// task 1004940: the weekly ask's SQL store — once per week by constraint, one pooled
|
|
104
108
|
// proposal per similar answer, the project guard on an answer. DB-free sibling:
|
|
105
109
|
// market_watch_ask.
|
|
@@ -190,6 +190,7 @@ const MODULE_GLOBS = {
|
|
|
190
190
|
// signed-out visitor. Its test sits at the repo root, listed by exact path.
|
|
191
191
|
'front-door-guide': [
|
|
192
192
|
'modules/front-door-guide/', 'tests/front_door_guide.mjs',
|
|
193
|
+
'tests/front_door_guide_plan.mjs', 'tests/front_door_guide_db.mjs',
|
|
193
194
|
],
|
|
194
195
|
// The /raise investor page (task 1004847, ADR 0384): hub-only.
|
|
195
196
|
raise: [
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.13'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// tests/front_door_guide_db.mjs — the start-a-project guide's SQL against a REAL Postgres
|
|
2
|
+
// (tasks 1005272, 1005273).
|
|
3
|
+
//
|
|
4
|
+
// What only a real engine can say: both migrations apply, and apply again as no-ops; a
|
|
5
|
+
// turn and a plan land in one statement each; only replies count as a visitor's messages;
|
|
6
|
+
// a chat attaches to one account and is never moved to another; and the retention sweep
|
|
7
|
+
// deletes an abandoned chat after the set number of days while its spend stays in the
|
|
8
|
+
// daily and monthly sums.
|
|
9
|
+
//
|
|
10
|
+
// The module is default:false, so CI's migrate.sh does not apply its migrations; this
|
|
11
|
+
// file applies them itself. Rows use a visitor hash and builder ids no real instance has,
|
|
12
|
+
// and are deleted before and after. Self-skips without a reachable Postgres; in the
|
|
13
|
+
// INTEGRATION set (scripts/bongos/run-unit-tests.js) so the DB-free lane never runs it.
|
|
14
|
+
//
|
|
15
|
+
// Run: DATABASE_URL=... node tests/front_door_guide_db.mjs
|
|
16
|
+
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { strict as assert } from 'node:assert';
|
|
20
|
+
import { insertTestBuilders } from './helpers.mjs';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const { Pool } = require('pg');
|
|
24
|
+
const db = require('../modules/front-door-guide/db.js');
|
|
25
|
+
const sql = (n) => readFileSync(new URL(`../modules/front-door-guide/migrations/${n}`, import.meta.url), 'utf8');
|
|
26
|
+
const M1 = sql('front_door_guide_001_conversations.sql');
|
|
27
|
+
const M2 = sql('front_door_guide_002_plan_and_account.sql');
|
|
28
|
+
|
|
29
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
30
|
+
try {
|
|
31
|
+
await pool.query('SELECT 1');
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.log(`front_door_guide_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
34
|
+
await pool.end().catch(() => {});
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const x = (text, params) => pool.query(text, params);
|
|
39
|
+
const V = 't1005273-visitor';
|
|
40
|
+
const ALICE = 91005273, BOB = 91005274;
|
|
41
|
+
|
|
42
|
+
async function cleanup() {
|
|
43
|
+
await x(`DELETE FROM front_door_guide_calls WHERE visitor_hash LIKE $1`, [`${V}%`]).catch(() => {});
|
|
44
|
+
await x(`DELETE FROM front_door_guide_conversations WHERE visitor_hash LIKE $1`, [`${V}%`]).catch(() => {});
|
|
45
|
+
await x('DELETE FROM builders WHERE id = ANY($1)', [[ALICE, BOB]]).catch(() => {});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const call = (over = {}) => ({ visitorHash: V, model: 'claude-opus-5-5', inputTokens: 800, outputTokens: 200, cacheReadTokens: 0, cacheWriteTokens: 0, costMicro: 9000, stopReason: 'end_turn', ...over });
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
for (const m of [M1, M2, M1, M2]) await x(m); // applies, then re-applies as a no-op
|
|
52
|
+
await cleanup();
|
|
53
|
+
await insertTestBuilders(pool, [{ id: ALICE, login: 't1005273-alice' }, { id: BOB, login: 't1005273-bob' }]);
|
|
54
|
+
|
|
55
|
+
const dayBefore = await db.siteSpendMicro('day', x);
|
|
56
|
+
|
|
57
|
+
// A turn and a plan.
|
|
58
|
+
const conv = await db.createConversation('hash-a-1005273', V, x);
|
|
59
|
+
const turned = await db.recordTurn(conv.id, { messages: [{ role: 'user', content: 'hi' }, { role: 'assistant', content: 'hello' }], status: 'open', endReason: null }, call(), x);
|
|
60
|
+
assert.equal(turned.visitor_messages, 1);
|
|
61
|
+
assert.equal(Number(turned.spend_micro), 9000);
|
|
62
|
+
const planned = await db.savePlan(conv.id, { summary: 'A plan' }, call({ costMicro: 20000 }), x);
|
|
63
|
+
assert.equal(planned.plan.summary, 'A plan');
|
|
64
|
+
assert.equal(planned.status, 'ended');
|
|
65
|
+
assert.equal(planned.end_reason, 'planned');
|
|
66
|
+
const again = await db.savePlan(conv.id, { summary: 'Overwrite?' }, call({ costMicro: 1 }), x);
|
|
67
|
+
assert.equal(again.plan.summary, 'A plan', 'a stored plan is never overwritten');
|
|
68
|
+
await db.recordPlanSpend(conv.id, call({ costMicro: 5 }), x);
|
|
69
|
+
|
|
70
|
+
const usage = await db.visitorUsage(V, x);
|
|
71
|
+
assert.equal(usage.messages, 1, 'only replies are messages');
|
|
72
|
+
assert.equal(usage.spendMicro, 9000 + 20000 + 1 + 5);
|
|
73
|
+
|
|
74
|
+
// Attach: the token holder's account, once; never moved to another.
|
|
75
|
+
assert.equal(await db.attachToBuilder(conv.id, 'wrong-hash', ALICE, x), null);
|
|
76
|
+
const mine = await db.attachToBuilder(conv.id, 'hash-a-1005273', ALICE, x);
|
|
77
|
+
assert.equal(Number(mine.builder_id), ALICE);
|
|
78
|
+
assert.ok(mine.attached_at);
|
|
79
|
+
assert.equal(await db.attachToBuilder(conv.id, 'hash-a-1005273', BOB, x), null);
|
|
80
|
+
assert.equal((await db.latestForBuilder(ALICE, x)).plan.summary, 'A plan');
|
|
81
|
+
assert.equal(await db.latestForBuilder(BOB, x), null);
|
|
82
|
+
|
|
83
|
+
// The sweep: an abandoned chat older than the setting goes; a recent one and an
|
|
84
|
+
// attached one stay; the abandoned chat's spend stays on the books.
|
|
85
|
+
const abandoned = await db.createConversation('hash-b-1005273', V, x);
|
|
86
|
+
await db.recordTurn(abandoned.id, { messages: [], status: 'open', endReason: null }, call({ costMicro: 7000 }), x);
|
|
87
|
+
const recent = await db.createConversation('hash-c-1005273', V, x);
|
|
88
|
+
await x(`UPDATE front_door_guide_conversations SET updated_at = now() - interval '15 days' WHERE id = ANY($1)`, [[abandoned.id, conv.id]]);
|
|
89
|
+
await x(`UPDATE front_door_guide_conversations SET updated_at = now() - interval '13 days' WHERE id = $1`, [recent.id]);
|
|
90
|
+
const before = await db.siteSpendMicro('day', x);
|
|
91
|
+
|
|
92
|
+
const deleted = await db.deleteAbandoned(14, x);
|
|
93
|
+
assert.ok(deleted >= 1);
|
|
94
|
+
const left = (await x(`SELECT id FROM front_door_guide_conversations WHERE visitor_hash = $1 ORDER BY id`, [V])).rows.map((r) => String(r.id));
|
|
95
|
+
assert.deepEqual(left, [String(conv.id), String(recent.id)], 'the abandoned chat is gone; the attached and the recent stay');
|
|
96
|
+
const orphan = await x(`SELECT conversation_id, cost_micro FROM front_door_guide_calls WHERE visitor_hash = $1 AND cost_micro = 7000`, [V]);
|
|
97
|
+
assert.equal(orphan.rows.length, 1);
|
|
98
|
+
assert.equal(orphan.rows[0].conversation_id, null);
|
|
99
|
+
assert.equal(await db.siteSpendMicro('day', x), before, 'the budgets still count the deleted chat\'s money');
|
|
100
|
+
assert.ok(before >= dayBefore + 9000 + 20000 + 1 + 5 + 7000);
|
|
101
|
+
|
|
102
|
+
// Refusals the schema owns.
|
|
103
|
+
await assert.rejects(x(`INSERT INTO front_door_guide_calls (visitor_hash, model, cost_micro, kind) VALUES ($1, 'm', 1, 'other')`, [V]));
|
|
104
|
+
await assert.rejects(x(`INSERT INTO front_door_guide_calls (visitor_hash, model, cost_micro) VALUES ($1, 'm', -1)`, [V]));
|
|
105
|
+
|
|
106
|
+
console.log('front_door_guide_db.mjs: all checks passed');
|
|
107
|
+
} finally {
|
|
108
|
+
await cleanup();
|
|
109
|
+
await pool.end();
|
|
110
|
+
}
|