|
|
@@ -42,6 +42,7 @@ import { |
| 42 | 42 |
markServing, |
| 43 | 43 |
noteRunProgress, |
| 44 | 44 |
previewLatency, |
| 45 |
STOPPED_SUMMARY, |
| 45 | 46 |
type Run, |
| 46 | 47 |
} from "../src/core/runs.ts"; |
| 47 | 48 |
import { createUser, getUserByName, type User } from "../src/core/users.ts"; |
|
|
@@ -6086,6 +6087,51 @@ describe("previewLatency: the median seconds from push to serving", () => { |
| 6086 | 6087 |
expect(latency.failedN).toBe(1); |
| 6087 | 6088 |
expect(latency.medianSeconds).toBeUndefined(); |
| 6088 | 6089 |
}); |
| 6090 |
|
| 6091 |
test("a deploy the forge's own restart cancelled is never counted as failed, nor in the total it is 'of' (#255, #274)", () => { |
| 6092 |
const now = Date.now() + 40_000_000; |
| 6093 |
const since = new Date(now - 60_000).toISOString(); |
| 6094 |
|
| 6095 |
|
| 6096 |
const failed = createRun(app.db, { repoId: repo.id, kind: "deploy" }); |
| 6097 |
app.db |
| 6098 |
.query("UPDATE run SET created_at = ?2 WHERE id = ?1") |
| 6099 |
.run(failed.id, new Date(now).toISOString()); |
| 6100 |
claimRun(app.db, failed.id); |
| 6101 |
finishRun(app.db, failed.id, { state: "failed", summary: "build failed" }); |
| 6102 |
|
| 6103 |
|
| 6104 |
|
| 6105 |
const stopped = createRun(app.db, { repoId: repo.id, kind: "deploy" }); |
| 6106 |
app.db |
| 6107 |
.query("UPDATE run SET created_at = ?2 WHERE id = ?1") |
| 6108 |
.run(stopped.id, new Date(now + 1_000).toISOString()); |
| 6109 |
claimRun(app.db, stopped.id); |
| 6110 |
app.db |
| 6111 |
.query( |
| 6112 |
"UPDATE run SET state = 'cancelled', summary = ?2, finished_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?1", |
| 6113 |
) |
| 6114 |
.run(stopped.id, STOPPED_SUMMARY); |
| 6115 |
|
| 6116 |
const latency = previewLatency(app.db, repo.id, since)!; |
| 6117 |
expect(latency.n).toBe(0); |
| 6118 |
expect(latency.failedN).toBe(1); |
| 6119 |
|
| 6120 |
|
| 6121 |
|
| 6122 |
const byPerson = createRun(app.db, { repoId: repo.id, kind: "deploy" }); |
| 6123 |
app.db |
| 6124 |
.query("UPDATE run SET created_at = ?2 WHERE id = ?1") |
| 6125 |
.run(byPerson.id, new Date(now + 2_000).toISOString()); |
| 6126 |
claimRun(app.db, byPerson.id); |
| 6127 |
app.db |
| 6128 |
.query( |
| 6129 |
"UPDATE run SET state = 'cancelled', summary = 'cancelled by alice', finished_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?1", |
| 6130 |
) |
| 6131 |
.run(byPerson.id); |
| 6132 |
const withPerson = previewLatency(app.db, repo.id, since)!; |
| 6133 |
expect(withPerson.failedN).toBe(2); |
| 6134 |
}); |
| 6089 | 6135 |
}); |
| 6090 | 6136 |
|
| 6091 | 6137 |
describe("the studio's own words for how fast a change reaches its preview", () => { |
|
|
@@ -6136,6 +6182,15 @@ describe("the studio's own words for how fast a change reaches its preview", () |
| 6136 | 6182 |
if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); |
| 6137 | 6183 |
const enabled = await enableApp(app, alice, repo, { slug: "trickle" }); |
| 6138 | 6184 |
if (!enabled.ok) throw new Error(String(enabled.error)); |
| 6185 |
|
| 6186 |
|
| 6187 |
|
| 6188 |
const seedRun = createRun(app.db, { repoId: repo.id, kind: "agent" }); |
| 6189 |
app.db |
| 6190 |
.query("UPDATE run SET created_at = ?2 WHERE id = ?1") |
| 6191 |
.run(seedRun.id, new Date(Date.now() - 30 * 24 * 60 * 60_000).toISOString()); |
| 6192 |
claimRun(app.db, seedRun.id); |
| 6193 |
finishRun(app.db, seedRun.id, { state: "ok", summary: "seed" }); |
| 6139 | 6194 |
}); |
| 6140 | 6195 |
|
| 6141 | 6196 |
afterAll(async () => { |
|
|
@@ -6253,6 +6308,95 @@ describe("the studio's own words for how fast a change reaches its preview", () |
| 6253 | 6308 |
}); |
| 6254 | 6309 |
}); |
| 6255 | 6310 |
|
| 6311 |
describe("a low deploy count never reads as a failure ratio (#274)", () => { |
| 6312 |
let dataDir: string; |
| 6313 |
let app: App; |
| 6314 |
let servers: Servers; |
| 6315 |
let base: string; |
| 6316 |
let alice: User; |
| 6317 |
let repo: Repo; |
| 6318 |
let ownerCookie: string; |
| 6319 |
|
| 6320 |
beforeAll(async () => { |
| 6321 |
dataDir = await mkdtemp(join(tmpdir(), "forge-low-count-ratio-")); |
| 6322 |
app = createApp({ dataDir, port: 0 }); |
| 6323 |
servers = await serve(app); |
| 6324 |
base = servers.url; |
| 6325 |
const a = await createUser(app.db, { |
| 6326 |
username: "alice", |
| 6327 |
email: "[email protected]", |
| 6328 |
password: "a-good-passphrase", |
| 6329 |
adminIfFirst: true, |
| 6330 |
}); |
| 6331 |
if (!a.ok) throw new Error(a.error); |
| 6332 |
alice = a.value; |
| 6333 |
ownerCookie = ( |
| 6334 |
( |
| 6335 |
await fetch(`${base}/signin`, { |
| 6336 |
method: "POST", |
| 6337 |
body: new URLSearchParams({ |
| 6338 |
username: "alice", |
| 6339 |
password: "a-good-passphrase", |
| 6340 |
}), |
| 6341 |
redirect: "manual", |
| 6342 |
}) |
| 6343 |
).headers.get("set-cookie") ?? "" |
| 6344 |
).split(";")[0]!; |
| 6345 |
const made = await createRepo(app, alice, { |
| 6346 |
name: "tworuns", |
| 6347 |
visibility: "private", |
| 6348 |
description: "", |
| 6349 |
}); |
| 6350 |
if (!made.ok) throw new Error(String(made.error)); |
| 6351 |
repo = made.value; |
| 6352 |
const seeded = await commitToBranch(app, alice, repo, { |
| 6353 |
message: "Start", |
| 6354 |
files: [{ path: "README.md", content: "start\n" }], |
| 6355 |
}); |
| 6356 |
if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); |
| 6357 |
const enabled = await enableApp(app, alice, repo, { slug: "tworuns" }); |
| 6358 |
if (!enabled.ok) throw new Error(String(enabled.error)); |
| 6359 |
|
| 6360 |
|
| 6361 |
|
| 6362 |
const seedRun = createRun(app.db, { repoId: repo.id, kind: "agent" }); |
| 6363 |
app.db |
| 6364 |
.query("UPDATE run SET created_at = ?2 WHERE id = ?1") |
| 6365 |
.run(seedRun.id, new Date(Date.now() - 30 * 24 * 60 * 60_000).toISOString()); |
| 6366 |
claimRun(app.db, seedRun.id); |
| 6367 |
finishRun(app.db, seedRun.id, { state: "ok", summary: "seed" }); |
| 6368 |
|
| 6369 |
for (let i = 0; i < 2; i++) { |
| 6370 |
const failed = createRun(app.db, { |
| 6371 |
repoId: repo.id, |
| 6372 |
kind: "deploy", |
| 6373 |
env: "production", |
| 6374 |
reason: "push", |
| 6375 |
}); |
| 6376 |
claimRun(app.db, failed.id); |
| 6377 |
finishRun(app.db, failed.id, { state: "failed", summary: "build failed" }); |
| 6378 |
} |
| 6379 |
}); |
| 6380 |
|
| 6381 |
afterAll(async () => { |
| 6382 |
servers.stop(); |
| 6383 |
app.close(); |
| 6384 |
await rm(dataDir, { recursive: true, force: true }); |
| 6385 |
}); |
| 6386 |
|
| 6387 |
const get = (path: string) => |
| 6388 |
fetch(`${base}${path}`, { |
| 6389 |
headers: { cookie: `${ownerCookie}; forge_mode=studio` }, |
| 6390 |
redirect: "manual", |
| 6391 |
}); |
| 6392 |
|
| 6393 |
test("2 of 2 never reads as a ratio — a handful of deploys needs to be behind it first", async () => { |
| 6394 |
const page = await (await get("/alice/tworuns")).text(); |
| 6395 |
expect(page).toContain("not enough deploys this week to say"); |
| 6396 |
expect(page).not.toContain("never reached a preview"); |
| 6397 |
}); |
| 6398 |
}); |
| 6399 |
|
| 6256 | 6400 |
describe("this week's runs and spend are counted by the database, not read off a page of runs", () => { |
| 6257 | 6401 |
let dataDir: string; |
| 6258 | 6402 |
let app: App; |
|
|
@@ -6260,6 +6404,7 @@ describe("this week's runs and spend are counted by the database, not read off a |
| 6260 | 6404 |
let base: string; |
| 6261 | 6405 |
let alice: User; |
| 6262 | 6406 |
let busy: Repo; |
| 6407 |
let quiet: Repo; |
| 6263 | 6408 |
let ownerCookie: string; |
| 6264 | 6409 |
|
| 6265 | 6410 |
|
|
|
@@ -6308,8 +6453,19 @@ describe("this week's runs and spend are counted by the database, not read off a |
| 6308 | 6453 |
const enabled = await enableApp(app, alice, made.value, { slug: name }); |
| 6309 | 6454 |
if (!enabled.ok) throw new Error(String(enabled.error)); |
| 6310 | 6455 |
if (name === "busy") busy = made.value; |
| 6456 |
if (name === "quiet") quiet = made.value; |
| 6311 | 6457 |
} |
| 6312 | 6458 |
|
| 6459 |
|
| 6460 |
|
| 6461 |
|
| 6462 |
const oldQuiet = createRun(app.db, { repoId: quiet.id, kind: "agent" }); |
| 6463 |
claimRun(app.db, oldQuiet.id); |
| 6464 |
finishRun(app.db, oldQuiet.id, { state: "ok", summary: "long ago" }); |
| 6465 |
app.db |
| 6466 |
.query("UPDATE run SET created_at = ?2 WHERE id = ?1") |
| 6467 |
.run(oldQuiet.id, new Date(Date.now() - 30 * 24 * 60 * 60_000).toISOString()); |
| 6468 |
|
| 6313 | 6469 |
const keeper = await createAgent(app, alice, { username: "keeper" }); |
| 6314 | 6470 |
if (!keeper.ok) throw new Error(String(keeper.error)); |
| 6315 | 6471 |
expect(addToCrew(app, alice, busy, "keeper").ok).toBe(true); |
|
|
@@ -6377,7 +6533,7 @@ describe("this week's runs and spend are counted by the database, not read off a |
| 6377 | 6533 |
expect(page).not.toContain("$99.00"); |
| 6378 | 6534 |
}); |
| 6379 | 6535 |
|
| 6380 | |
test("an app with no runs this week reads 0 and $0.00, never blank", async () => { |
| 6536 |
test("an app with real history but no runs this week reads 0 and $0.00, never blank", async () => { |
| 6381 | 6537 |
const page = await (await get("/alice/quiet")).text(); |
| 6382 | 6538 |
expect(page).toMatch(/Runs<\/dt>\s*<dd>0<\/dd>/); |
| 6383 | 6539 |
expect(page).toContain("$0.00"); |
|
|
@@ -6391,6 +6547,7 @@ describe("this week's shipping is counted by the database, not read off a page o |
| 6391 | 6547 |
let base: string; |
| 6392 | 6548 |
let alice: User; |
| 6393 | 6549 |
let busy: Repo; |
| 6550 |
let quiet: Repo; |
| 6394 | 6551 |
let ownerCookie: string; |
| 6395 | 6552 |
|
| 6396 | 6553 |
|
|
|
@@ -6455,6 +6612,7 @@ describe("this week's shipping is counted by the database, not read off a page o |
| 6455 | 6612 |
const enabled = await enableApp(app, alice, made.value, { slug: name }); |
| 6456 | 6613 |
if (!enabled.ok) throw new Error(String(enabled.error)); |
| 6457 | 6614 |
if (name === "busy") busy = made.value; |
| 6615 |
if (name === "quiet") quiet = made.value; |
| 6458 | 6616 |
} |
| 6459 | 6617 |
|
| 6460 | 6618 |
|
|
|
@@ -6470,6 +6628,15 @@ describe("this week's shipping is counted by the database, not read off a page o |
| 6470 | 6628 |
new Date(now - 10 * 24 * 60 * 60_000).toISOString(), |
| 6471 | 6629 |
); |
| 6472 | 6630 |
|
| 6631 |
|
| 6632 |
|
| 6633 |
|
| 6634 |
seedPull( |
| 6635 |
quiet, |
| 6636 |
"shipped a month ago", |
| 6637 |
new Date(now - 30 * 24 * 60 * 60_000).toISOString(), |
| 6638 |
); |
| 6639 |
|
| 6473 | 6640 |
const keeper = await createAgent(app, alice, { username: "keeper" }); |
| 6474 | 6641 |
if (!keeper.ok) throw new Error(String(keeper.error)); |
| 6475 | 6642 |
expect(addToCrew(app, alice, busy, "keeper").ok).toBe(true); |
|
|
@@ -6521,13 +6688,118 @@ describe("this week's shipping is counted by the database, not read off a page o |
| 6521 | 6688 |
expect(page).toContain(`$${(SPENT / SHIPPED).toFixed(2)} per shipped change`); |
| 6522 | 6689 |
}); |
| 6523 | 6690 |
|
| 6524 | |
test("an app that shipped nothing this week reads 0 changes and no cost per change", async () => { |
| 6691 |
test("an app with real history but nothing shipped this week reads 0 changes and no cost per change", async () => { |
| 6525 | 6692 |
const page = await (await get("/alice/quiet")).text(); |
| 6526 | 6693 |
expect(page).toMatch(/Shipped<\/dt>\s*<dd>0 changes<\/dd>/); |
| 6527 | 6694 |
expect(page).not.toContain("per shipped change"); |
| 6528 | 6695 |
}); |
| 6529 | 6696 |
}); |
| 6530 | 6697 |
|
| 6698 |
describe("a one-minute-old app's front door does not lead with a week of zeros (#274)", () => { |
| 6699 |
let dataDir: string; |
| 6700 |
let app: App; |
| 6701 |
let servers: Servers; |
| 6702 |
let base: string; |
| 6703 |
let alice: User; |
| 6704 |
let repo: Repo; |
| 6705 |
let ownerCookie: string; |
| 6706 |
|
| 6707 |
beforeAll(async () => { |
| 6708 |
dataDir = await mkdtemp(join(tmpdir(), "forge-day-one-")); |
| 6709 |
app = createApp({ dataDir, port: 0 }); |
| 6710 |
servers = await serve(app); |
| 6711 |
base = servers.url; |
| 6712 |
const a = await createUser(app.db, { |
| 6713 |
username: "alice", |
| 6714 |
email: "[email protected]", |
| 6715 |
password: "a-good-passphrase", |
| 6716 |
adminIfFirst: true, |
| 6717 |
}); |
| 6718 |
if (!a.ok) throw new Error(a.error); |
| 6719 |
alice = a.value; |
| 6720 |
ownerCookie = ( |
| 6721 |
( |
| 6722 |
await fetch(`${base}/signin`, { |
| 6723 |
method: "POST", |
| 6724 |
body: new URLSearchParams({ |
| 6725 |
username: "alice", |
| 6726 |
password: "a-good-passphrase", |
| 6727 |
}), |
| 6728 |
redirect: "manual", |
| 6729 |
}) |
| 6730 |
).headers.get("set-cookie") ?? "" |
| 6731 |
).split(";")[0]!; |
| 6732 |
const made = await createRepo(app, alice, { |
| 6733 |
name: "newborn", |
| 6734 |
visibility: "public", |
| 6735 |
description: "", |
| 6736 |
}); |
| 6737 |
if (!made.ok) throw new Error(String(made.error)); |
| 6738 |
repo = made.value; |
| 6739 |
const seeded = await commitToBranch(app, alice, repo, { |
| 6740 |
message: "Start", |
| 6741 |
files: [{ path: "README.md", content: "start\n" }], |
| 6742 |
}); |
| 6743 |
if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); |
| 6744 |
const enabled = await enableApp(app, alice, repo, { slug: "newborn" }); |
| 6745 |
if (!enabled.ok) throw new Error(String(enabled.error)); |
| 6746 |
|
| 6747 |
|
| 6748 |
|
| 6749 |
|
| 6750 |
const cancelledByForge = createRun(app.db, { |
| 6751 |
repoId: repo.id, |
| 6752 |
kind: "deploy", |
| 6753 |
env: "production", |
| 6754 |
reason: "boot", |
| 6755 |
}); |
| 6756 |
claimRun(app.db, cancelledByForge.id); |
| 6757 |
app.db |
| 6758 |
.query( |
| 6759 |
"UPDATE run SET state = 'cancelled', summary = ?2, finished_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?1", |
| 6760 |
) |
| 6761 |
.run(cancelledByForge.id, STOPPED_SUMMARY); |
| 6762 |
const secondBoot = createRun(app.db, { |
| 6763 |
repoId: repo.id, |
| 6764 |
kind: "deploy", |
| 6765 |
env: "production", |
| 6766 |
reason: "push", |
| 6767 |
}); |
| 6768 |
claimRun(app.db, secondBoot.id); |
| 6769 |
}); |
| 6770 |
|
| 6771 |
afterAll(async () => { |
| 6772 |
servers.stop(); |
| 6773 |
app.close(); |
| 6774 |
await rm(dataDir, { recursive: true, force: true }); |
| 6775 |
}); |
| 6776 |
|
| 6777 |
const get = (path: string) => |
| 6778 |
fetch(`${base}${path}`, { |
| 6779 |
headers: { cookie: `${ownerCookie}; forge_mode=studio` }, |
| 6780 |
redirect: "manual", |
| 6781 |
}); |
| 6782 |
|
| 6783 |
test("the day-one page a person lands on from New app says one true line, not a week of zeros", async () => { |
| 6784 |
const page = await (await get("/alice/newborn")).text(); |
| 6785 |
expect(page).toContain("This week"); |
| 6786 |
expect(page).toContain("Nothing to show yet"); |
| 6787 |
|
| 6788 |
|
| 6789 |
|
| 6790 |
expect(page).not.toMatch(/Runs<\/dt>/); |
| 6791 |
expect(page).not.toMatch(/Spent<\/dt>/); |
| 6792 |
expect(page).not.toContain("never reached a preview"); |
| 6793 |
expect(page).not.toContain("$0.00"); |
| 6794 |
}); |
| 6795 |
|
| 6796 |
test("a stranger sees the same quiet line, not the owner's zeros", async () => { |
| 6797 |
const page = await (await fetch(`${base}/alice/newborn`)).text(); |
| 6798 |
expect(page).toContain("Nothing to show yet"); |
| 6799 |
expect(page).not.toMatch(/Shipped<\/dt>\s*<dd>0 changes<\/dd>/); |
| 6800 |
}); |
| 6801 |
}); |
| 6802 |
|
| 6531 | 6803 |
describe("finding things in the studio: /search", () => { |
| 6532 | 6804 |
let dataDir: string; |
| 6533 | 6805 |
let app: App; |