Chat cards, dashboard templates and billing leave issues behind
Stored chat cards become work_item and draft_work_item, dashboard templates read the work_items dataset, and billing drops the issue caps nothing used.
- G1T-Session:
- ses_01m56adwzvenhby748vvbn96c5
10 files+116−110/10 viewed
| 164 | 164 | leads to `flagon-io`, Flagon, Inc. (seeded by identity's migration | |
| 165 | 165 | `0029_workspace_aliases.sql`), so nobody mistakes the trading name for | |
| 166 | 166 | the organization. Every address under an alias leads to the workspace: | |
| 167 | − | pages answer with a 301 to the same page (`/g1t/g1t/issues` to | |
| 168 | − | `/flagon-io/g1t/issues`), git over HTTPS is answered in place as the | |
| 167 | + | pages answer with a 301 to the same page (`/g1t/g1t/pulls` to | |
| 168 | + | `/flagon-io/g1t/pulls`), git over HTTPS is answered in place as the | |
| 169 | 169 | workspace's repository (pushes do not follow redirects), the API and MCP | |
| 170 | 170 | run the call again under the workspace's slug, and the package | |
| 171 | 171 | registries answer a 301 (308 for a publish). An alias points at the |
| 127 | 127 | }, | |
| 128 | 128 | }); | |
| 129 | 129 | assert.deepEqual( | |
| 130 | − | parseAllowances(form({ plan: "on", oss: "5", trial: "2.50", agents: "20", runCap: "25", issueCap: "500", auditDays: "365", hold: " Card disputed;\nwaiting on the bank " })), | |
| 130 | + | parseAllowances(form({ plan: "on", oss: "5", trial: "2.50", agents: "20", runCap: "25", auditDays: "365", hold: " Card disputed;\nwaiting on the bank " })), | |
| 131 | 131 | { | |
| 132 | 132 | ok: true, | |
| 133 | 133 | value: { |
| 193 | 193 | * (for the audit log, the plan's). A hold is a line saying why new compute | |
| 194 | 194 | * is held; blank is no hold. A run's cap is not staff's to set here: it is | |
| 195 | 195 | * always sent cleared, so the plan's applies. The overrides retired with | |
| 196 | − | * g1t's own agents (agents at once, an issue's cap) control nothing and are | |
| 197 | − | * not sent; billing keeps them empty. | |
| 196 | + | * g1t's own agents (agents at once) control nothing and are not sent; | |
| 197 | + | * billing keeps them empty. | |
| 198 | 198 | */ | |
| 199 | 199 | export function parseAllowances(form: FormData): Parsed<Allowances> { | |
| 200 | 200 | const amount = (name: string, what: string, max: number, maxText: string, min = 0): Parsed<number | null> => { |
| 149 | 149 | ||
| 150 | 150 | /** A card g1t or an agent posts: an event, a task, an approval. */ | |
| 151 | 151 | export type MessageCard = { | |
| 152 | − | /** What it is about, e.g. `pull`, `issue`, `task`, `deploy`, `approval`, `session`, `draft_issue`. */ | |
| 152 | + | /** What it is about, e.g. `pull`, `work_item`, `task`, `deploy`, `approval`, `session`, `draft_work_item`. */ | |
| 153 | 153 | kind: string; | |
| 154 | 154 | title: string; | |
| 155 | 155 | /** A short line under the title: "3/3 checks · +214 −87". */ | |
| ⋯ | |||
| 158 | 158 | state: string | null; | |
| 159 | 159 | /** Where clicking the card goes, relative to the site. */ | |
| 160 | 160 | href: string | null; | |
| 161 | − | /** A preview in Markdown under the title: a draft issue's body, a report's first lines. */ | |
| 161 | + | /** A preview in Markdown under the title: a draft work item's body, a report's first lines. */ | |
| 162 | 162 | body?: string | null; | |
| 163 | 163 | /** Labelled facts shown in two columns: "Repository · acme/web", "Cap · $2.00". */ | |
| 164 | 164 | fields?: CardField[]; | |
| 1 | + | -- Issues became work items (g1t #57), and the dataset `issues` is | |
| 2 | + | -- `work_items` now (packages/contracts/src/datasets.ts). A dashboard's | |
| 3 | + | -- tiles are kept in its Yjs document, which no dashboard has yet (the kind | |
| 4 | + | -- is not built: kinds/index.ts); a workspace's dashboard templates keep | |
| 5 | + | -- theirs as JSON (`{ tiles: [{ query: { dataset } }] }`), rewritten here. | |
| 6 | + | UPDATE folio_templates | |
| 7 | + | SET body = json_set(body, '$.tiles', ( | |
| 8 | + | SELECT json_group_array(json(tile)) FROM ( | |
| 9 | + | SELECT CASE WHEN json_extract(t.value, '$.query.dataset') = 'issues' | |
| 10 | + | THEN json_set(t.value, '$.query.dataset', 'work_items') | |
| 11 | + | ELSE t.value END AS tile | |
| 12 | + | FROM json_each(folio_templates.body, '$.tiles') t | |
| 13 | + | ORDER BY t.key | |
| 14 | + | ) | |
| 15 | + | )) | |
| 16 | + | WHERE kind = 'dashboard' AND json_valid(body) AND json_type(body, '$.tiles') = 'array' | |
| 17 | + | AND EXISTS (SELECT 1 FROM json_each(folio_templates.body, '$.tiles') t WHERE json_extract(t.value, '$.query.dataset') = 'issues'); |
| 1 | + | import assert from "node:assert/strict"; | |
| 2 | + | import { readFileSync, readdirSync } from "node:fs"; | |
| 3 | + | import { DatabaseSync } from "node:sqlite"; | |
| 4 | + | import { test } from "node:test"; | |
| 5 | + | ||
| 6 | + | const dir = new URL("../migrations/", import.meta.url); | |
| 7 | + | const MIGRATION = "0011_work_items_dataset.sql"; | |
| 8 | + | ||
| 9 | + | test("dashboard templates that read issues read work items after the migration, run once or twice", () => { | |
| 10 | + | const db = new DatabaseSync(":memory:"); | |
| 11 | + | for (const file of readdirSync(dir).sort()) { | |
| 12 | + | if (file >= MIGRATION) break; | |
| 13 | + | db.exec(readFileSync(new URL(file, dir), "utf8")); | |
| 14 | + | } | |
| 15 | + | const insert = db.prepare( | |
| 16 | + | "INSERT INTO folio_templates (id, workspace_id, kind, name, body, created_by, created_at) VALUES (?, 'wsp_1', ?, ?, ?, 'user:usr_1', 'now')", | |
| 17 | + | ); | |
| 18 | + | const tiles = [ | |
| 19 | + | { id: "t1", type: "stat", query: { dataset: "issues", measure: { op: "count" } } }, | |
| 20 | + | { id: "t2", type: "markdown", query: null, markdown: "Notes" }, | |
| 21 | + | { id: "t3", type: "line", query: { dataset: "pull_requests", measure: { op: "count" } } }, | |
| 22 | + | ]; | |
| 23 | + | insert.run("tpl_1", "dashboard", "Triage", JSON.stringify({ tiles })); | |
| 24 | + | insert.run("tpl_2", "doc", "Notes", "# issues"); | |
| 25 | + | insert.run("tpl_3", "dashboard", "Not JSON", "not json"); | |
| 26 | + | const sql = readFileSync(new URL(MIGRATION, dir), "utf8"); | |
| 27 | + | db.exec(sql); | |
| 28 | + | db.exec(sql); | |
| 29 | + | const body = (id: string) => (db.prepare("SELECT body FROM folio_templates WHERE id = ?").get(id) as { body: string }).body; | |
| 30 | + | const migrated = JSON.parse(body("tpl_1")) as { tiles: { id: string; query: { dataset: string } | null }[] }; | |
| 31 | + | assert.deepEqual( | |
| 32 | + | migrated.tiles.map((tile) => [tile.id, tile.query?.dataset ?? null]), | |
| 33 | + | [ | |
| 34 | + | ["t1", "work_items"], | |
| 35 | + | ["t2", null], | |
| 36 | + | ["t3", "pull_requests"], | |
| 37 | + | ], | |
| 38 | + | ); | |
| 39 | + | assert.equal(body("tpl_2"), "# issues"); | |
| 40 | + | assert.equal(body("tpl_3"), "not json"); | |
| 41 | + | }); |
| 1 | + | -- The caps on what the agents on one issue may spend in all (0018) went | |
| 2 | + | -- with g1t's own agents and nothing reads them; issues are work items now. | |
| 3 | + | ALTER TABLE limits DROP COLUMN issue_cap_micros; | |
| 4 | + | ALTER TABLE billing_accounts DROP COLUMN issue_cap_micros; |
| 4 | 4 | //! here first (see `g1t_contracts::billing::ReserveArgs`): | |
| 5 | 5 | //! | |
| 6 | 6 | //! - **`entitlements`**: the workspace's plan, whether it may start compute | |
| 7 | − | //! at all, its caps (agents at once, a run's time and spend, an issue's | |
| 8 | − | //! spend), its ceiling and exposure, and whether compute is paused. | |
| 7 | + | //! at all, its caps (agents at once, a run's time and spend), its | |
| 8 | + | //! ceiling and exposure, and whether compute is paused. | |
| 9 | 9 | //! - **`reserve`**: holds the work's estimated cost against what may pay | |
| 10 | 10 | //! for it (the plan's included usage, the trial, the open-source pool, | |
| 11 | 11 | //! then on-demand room under the ceiling and the spend limit), so starts | |
| ⋯ | |||
| 919 | 919 | } | |
| 920 | 920 | } | |
| 921 | 921 | ||
| 922 | − | /// Whether owners' caps are in bounds: a run $0.10 to $100 (the | |
| 923 | − | /// guardrails' most), an issue $1 to $1,000. | |
| 924 | 922 | pub(crate) fn paid_by_text(paid_by: PaidBy) -> &'static str { | |
| 925 | 923 | match paid_by { | |
| 926 | 924 | PaidBy::Credit => "credit", | |
| 1 | + | -- Issues became work items (g1t #57): cards kept about one are about a | |
| 2 | + | -- work item now, and a draft of one is a draft work item. | |
| 3 | + | UPDATE messages SET card = json_set(card, '$.kind', 'work_item') | |
| 4 | + | WHERE kind = 'card' AND json_valid(card) AND json_extract(card, '$.kind') = 'issue'; | |
| 5 | + | ||
| 6 | + | UPDATE messages SET card = json_set(card, '$.kind', 'draft_work_item') | |
| 7 | + | WHERE kind = 'card' AND json_valid(card) AND json_extract(card, '$.kind') = 'draft_issue'; |
| 1 | + | import assert from "node:assert/strict"; | |
| 2 | + | import { readFileSync, readdirSync } from "node:fs"; | |
| 3 | + | import { DatabaseSync } from "node:sqlite"; | |
| 4 | + | import { test } from "node:test"; | |
| 5 | + | ||
| 6 | + | const dir = new URL("../migrations/", import.meta.url); | |
| 7 | + | const MIGRATION = "0008_work_item_cards.sql"; | |
| 8 | + | ||
| 9 | + | test("cards kept about issues are about work items after the migration, run once or twice", () => { | |
| 10 | + | const db = new DatabaseSync(":memory:"); | |
| 11 | + | for (const file of readdirSync(dir).sort()) { | |
| 12 | + | if (file >= MIGRATION) break; | |
| 13 | + | db.exec(readFileSync(new URL(file, dir), "utf8")); | |
| 14 | + | } | |
| 15 | + | db.exec("INSERT INTO channels (id, workspace_id, kind, name, created_by, created_at) VALUES ('chn_1', 'wsp_1', 'channel', 'general', 'g1t', 'now')"); | |
| 16 | + | const insert = db.prepare( | |
| 17 | + | "INSERT INTO messages (id, channel_id, author, kind, body, card, created_at) VALUES (?, 'chn_1', 'g1t', ?, '', ?, 'now')", | |
| 18 | + | ); | |
| 19 | + | insert.run("msg_1", "card", JSON.stringify({ kind: "issue", title: "Header", detail: null, state: "Open", href: null })); | |
| 20 | + | insert.run("msg_2", "card", JSON.stringify({ kind: "draft_issue", title: "Draft", detail: null, state: "Draft", href: null })); | |
| 21 | + | insert.run("msg_3", "card", JSON.stringify({ kind: "pull", title: "Fix", detail: null, state: null, href: null })); | |
| 22 | + | insert.run("msg_4", "text", null); | |
| 23 | + | const sql = readFileSync(new URL(MIGRATION, dir), "utf8"); | |
| 24 | + | db.exec(sql); | |
| 25 | + | db.exec(sql); | |
| 26 | + | const kinds = db | |
| 27 | + | .prepare("SELECT id, json_extract(card, '$.kind') AS kind, json_extract(card, '$.title') AS title FROM messages ORDER BY id") | |
| 28 | + | .all(); | |
| 29 | + | assert.deepEqual( | |
| 30 | + | kinds.map((row) => [row.id, row.kind, row.title]), | |
| 31 | + | [ | |
| 32 | + | ["msg_1", "work_item", "Header"], | |
| 33 | + | ["msg_2", "draft_work_item", "Draft"], | |
| 34 | + | ["msg_3", "pull", "Fix"], | |
| 35 | + | ["msg_4", null, null], | |
| 36 | + | ], | |
| 37 | + | ); | |
| 38 | + | }); |
Comments
Sign in to comment.