Skip to content
Merged
35 changes: 33 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,46 @@ jobs:
typecheck-${{ runner.os }}-${{ hashFiles('bun.lock', '.bun-version', 'tsconfig.base.json', 'scripts/generate-tsconfig-references.ts') }}-
- run: bun run typecheck

build-test:
# A hosted runner's 4 vCPUs cap how much a single job's package fan-out
# (scripts/run-all.ts's own concurrency) can cut wall-clock: the total
# CPU-seconds across every package's test script divided by 4 was still
# over 90s. Splitting the same total work across 3 runners, each with
# its own 4 vCPUs, is what actually buys more parallelism. --shard i/3
# (scripts/run-all.ts) round-robins the workspace's stably name-sorted
# package list across shards, so a run's set of packages-per-shard is
# reproducible and every package lands in exactly one shard.
build-test-shard:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/setup-workbench
- run: bun run build
- run: bun run test
# The root-level scripts/*.test.ts suite (scripts/run-all.test.ts
# among them) isn't itself sharded — it only needs to run once, so
# it rides along on shard 1.
- if: matrix.shard == 1
run: bun test ./scripts/*.test.ts
- run: bun run scripts/run-all.ts test --shard ${{ matrix.shard }}/3

# The required check every branch protection rule and PR template
# names is "build-test" (predating the shard split above); this job
# keeps that name live by depending on every shard and failing if any
# of them did.
build-test:
needs: build-test-shard
if: always()
runs-on: ubuntu-latest
steps:
- if: contains(needs.build-test-shard.result, 'failure') || contains(needs.build-test-shard.result, 'cancelled')
run: |
echo "at least one build-test shard failed" >&2
exit 1

structural:
runs-on: ubuntu-latest
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ check:structural <name>`.
`docker compose -f docker-compose.test.yml up -d`. On CI jobs that
provision Postgres (`e2e-suite`, `isolation`, `db-suites`), `CI=true`
turns a missing URL into a hard failure via `dbGate`. The unit
jobs (`build-test`, `structural`, `lint`, `typecheck`) never
provision Postgres and still skip.
jobs (`build-test-shard` — `build-test` itself is now a summary job
over its 3-way shard matrix — `structural`, `lint`, `typecheck`)
never provision Postgres and still skip.
- `HUB_DATA_DIR` — hub boot fails. This is required runtime config,
not a test flag.
- `CI` — set automatically by GitHub Actions (`true`). Not a caller
Expand Down
2 changes: 1 addition & 1 deletion apps/hub/src/default-tenant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import postgres from "postgres";
import { and, eq } from "drizzle-orm";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { setupDatabase } from "../../../scripts/db-setup";
import { dbGate } from "../../../scripts/e2e/db-gate";
import { createDB } from "@intx/db";
Expand Down
2 changes: 1 addition & 1 deletion packages/access-policy/test/store.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyAccessPolicyMigrations } from "../src/migrations";
import { createDrizzleAccessPolicyStore } from "../src/store";
import { dbGate } from "../../../scripts/e2e/db-gate";
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-directory/test/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { afterAll, beforeAll, expect, test } from "bun:test";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyAgentDirectoryMigrations } from "../src/migrations";
import { dbGate } from "../../../scripts/e2e/db-gate";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyAgentDirectoryMigrations } from "../src/migrations";
import { createDrizzleDefinitionSkillsStore } from "../src/skills-store";
import { dbGate } from "../../../scripts/e2e/db-gate";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { AssetService } from "@intx/hub-sessions";
import { CORBITS_TOOLS_REGISTRY } from "@corbits/tool-registry-publish";

import { dbTargetFromUrl } from "../../../scripts/db-setup";
import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { dbGate } from "../../../scripts/e2e/db-gate";
import { CapabilityOutOfInventoryError } from "../src/capability-inventory";
import { resolvePinnedVersion } from "../src/tool-package-version";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { createDB, runMigrations, dropSchema, schema } from "@intx/db";

import { dbTargetFromUrl } from "../../../scripts/db-setup";
import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { listVisibleAgentDefinitions } from "../src/visible-definitions";
import { dbGate } from "../../../scripts/e2e/db-gate";

Expand Down
56 changes: 28 additions & 28 deletions packages/agent-lifecycle/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ function waitForTicks(count = 1): Promise<void> {
if (remaining <= 0) {
resolve();
} else {
setTimeout(tick, 5);
setTimeout(tick, 40);
}
};
setTimeout(tick, 5);
setTimeout(tick, 40);
});
}

Expand All @@ -41,8 +41,8 @@ describe("createAgentLifecycle", () => {
const routable = new Set(["agent-1@t.test"]);

const lifecycle = createAgentLifecycle({
idleSleepMs: 10,
sweepIntervalMs: 5,
idleSleepMs: 80,
sweepIntervalMs: 40,
isRoutable: (address) => routable.has(address),
undeploy: async (address, reason) => {
undeployCalls.push({ address, reason });
Expand All @@ -59,13 +59,13 @@ describe("createAgentLifecycle", () => {
lifecycle.track("agent-1@t.test");
lifecycle.recordActivity("agent-1@t.test");

// First sweep tick happens quickly (sweepIntervalMs=5) but the
// instance is not idle for the full 10ms yet.
// First sweep tick happens quickly (sweepIntervalMs=40) but the
// instance is not idle for the full 80ms yet.
await waitForTicks(1);
expect(undeployCalls).toEqual([]);

// Wait past idleSleepMs and let another sweep tick run.
await new Promise((resolve) => setTimeout(resolve, 15));
await new Promise((resolve) => setTimeout(resolve, 120));
await waitForTicks(1);

expect(undeployCalls).toEqual([
Expand All @@ -79,7 +79,7 @@ describe("createAgentLifecycle", () => {

const lifecycle = createAgentLifecycle({
idleSleepMs: 1_000,
sweepIntervalMs: 5,
sweepIntervalMs: 40,
isRoutable: (address) => routable.has(address),
undeploy: async (address, reason) => {
undeployCalls.push({ address, reason });
Expand All @@ -101,8 +101,8 @@ describe("createAgentLifecycle", () => {
const routable = new Set(["agent-1@t.test"]);

const lifecycle = createAgentLifecycle({
idleSleepMs: 10,
sweepIntervalMs: 5,
idleSleepMs: 80,
sweepIntervalMs: 40,
isRoutable: (address) => routable.has(address),
undeploy: async (address, reason) => {
undeployCalls.push({ address, reason });
Expand All @@ -116,7 +116,7 @@ describe("createAgentLifecycle", () => {
lifecycle.track("agent-1@t.test");
lifecycle.recordActivity("agent-1@t.test");

await new Promise((resolve) => setTimeout(resolve, 15));
await new Promise((resolve) => setTimeout(resolve, 120));
await waitForTicks(1);

expect(undeployCalls).toEqual([]);
Expand All @@ -127,8 +127,8 @@ describe("createAgentLifecycle", () => {
const routable = new Set(["agent-1@t.test"]);

const lifecycle = createAgentLifecycle({
idleSleepMs: 10,
sweepIntervalMs: 5,
idleSleepMs: 80,
sweepIntervalMs: 40,
isRoutable: (address) => routable.has(address),
undeploy: async (address, reason) => {
undeployCalls.push({ address, reason });
Expand All @@ -142,7 +142,7 @@ describe("createAgentLifecycle", () => {
// swept.
lifecycle.recordActivity("agent-1@t.test");

await new Promise((resolve) => setTimeout(resolve, 15));
await new Promise((resolve) => setTimeout(resolve, 120));
await waitForTicks(1);

expect(undeployCalls).toEqual([]);
Expand All @@ -153,8 +153,8 @@ describe("createAgentLifecycle", () => {
const routable = new Set(["agent-1@t.test"]);

const lifecycle = createAgentLifecycle({
idleSleepMs: 10,
sweepIntervalMs: 5,
idleSleepMs: 80,
sweepIntervalMs: 40,
isRoutable: (address) => routable.has(address),
undeploy: async (address, reason) => {
undeployCalls.push({ address, reason });
Expand All @@ -174,7 +174,7 @@ describe("createAgentLifecycle", () => {
expect(undeployCalls).toEqual([]);

// Only after the grace-seeded clock itself goes idle does it sleep.
await new Promise((resolve) => setTimeout(resolve, 15));
await new Promise((resolve) => setTimeout(resolve, 120));
await waitForTicks(1);
expect(undeployCalls).toEqual([
{ address: "agent-1@t.test", reason: IDLE_HIBERNATE_UNDEPLOY_REASON },
Expand All @@ -186,8 +186,8 @@ describe("createAgentLifecycle", () => {
const routable = new Set<string>();

const lifecycle = createAgentLifecycle({
idleSleepMs: 10,
sweepIntervalMs: 5,
idleSleepMs: 80,
sweepIntervalMs: 40,
isRoutable: (address) => routable.has(address),
undeploy: async (address, reason) => {
undeployCalls.push({ address, reason });
Expand All @@ -200,7 +200,7 @@ describe("createAgentLifecycle", () => {
lifecycle.track("agent-1@t.test");
lifecycle.recordActivity("agent-1@t.test");

await new Promise((resolve) => setTimeout(resolve, 15));
await new Promise((resolve) => setTimeout(resolve, 120));
await waitForTicks(1);

expect(undeployCalls).toEqual([]);
Expand All @@ -214,8 +214,8 @@ describe("createAgentLifecycle", () => {
let maxConcurrentUndeploys = 0;

const lifecycle = createAgentLifecycle({
idleSleepMs: 10,
sweepIntervalMs: 5,
idleSleepMs: 80,
sweepIntervalMs: 40,
isRoutable: (address) => routable.has(address),
undeploy: async (address) => {
concurrentUndeploys += 1;
Expand All @@ -242,7 +242,7 @@ describe("createAgentLifecycle", () => {
// land while the first sweep's undeploy is still pending. If ticks
// overlapped, this would call undeploy on the same address a
// second time before the first has resolved.
await new Promise((resolve) => setTimeout(resolve, 15));
await new Promise((resolve) => setTimeout(resolve, 120));
await waitForTicks(1);
expect(undeployCalls).toEqual(["agent-1@t.test"]);

Expand All @@ -251,7 +251,7 @@ describe("createAgentLifecycle", () => {
expect(maxConcurrentUndeploys).toBe(1);

releaseUndeploy?.();
await new Promise((resolve) => setTimeout(resolve, 5));
await new Promise((resolve) => setTimeout(resolve, 40));
});

test("ensureAwake no-ops when already routable", async () => {
Expand Down Expand Up @@ -297,7 +297,7 @@ describe("createAgentLifecycle", () => {

// Give both calls a tick to have entered `ensureAwake` and observed
// the same pending wake.
await new Promise((resolve) => setTimeout(resolve, 5));
await new Promise((resolve) => setTimeout(resolve, 40));
expect(wakeCalls).toEqual(["agent-1@t.test"]);

resolveWake?.();
Expand Down Expand Up @@ -354,7 +354,7 @@ describe("createAgentLifecycle", () => {

const lifecycle = createAgentLifecycle({
idleSleepMs: 1_000,
wakeTimeoutMs: 20,
wakeTimeoutMs: 160,
isRoutable: (address) => routable.has(address),
undeploy: async () => undefined,
wake: async () => {
Expand Down Expand Up @@ -405,7 +405,7 @@ describe("createAgentLifecycle", () => {

const lifecycle = createAgentLifecycle({
idleSleepMs: 1_000,
wakeTimeoutMs: 20,
wakeTimeoutMs: 160,
isRoutable: (address) => routable.has(address),
undeploy: async () => undefined,
wake: async (address) => {
Expand All @@ -421,7 +421,7 @@ describe("createAgentLifecycle", () => {

const first = lifecycle.ensureAwake("agent-1@t.test");

// The first caller's own 20ms budget expires while the underlying
// The first caller's own 160ms budget expires while the underlying
// wake is still running.
await expect(first).rejects.toThrow(/wake/i);
expect(wakeCalls).toBe(1);
Expand Down
2 changes: 1 addition & 1 deletion packages/bench/test/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { afterAll, beforeAll, expect, test } from "bun:test";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyBenchMigrations } from "../src/migrations";
import { createPostgresBenchSettingsStore } from "../src/pg-store";
import { dbGate } from "../../../scripts/e2e/db-gate";
Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/agent-turns.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import {
AGENT_TURN_STALE_MS,
createDrizzleAgentTurnStore,
Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/block-responses.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyChatMigrations } from "../src/migrations";
import { createDrizzleBlockResponseStore } from "../src/block-responses";
import type { BlockResponseKey } from "../src/block-responses";
Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/mailbox-fanout.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import postgres from "postgres";
import { sql } from "drizzle-orm";
import { createMailboxDb, runMailboxMigrations } from "@corbits/mailbox";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { dbGate } from "../../../scripts/e2e/db-gate";
import {
createDrizzleMailboxWriter,
Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { afterAll, beforeAll, expect, test } from "bun:test";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyChatMigrations, chatMigrations } from "../src/migrations";
import { dbGate } from "../../../scripts/e2e/db-gate";

Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/reactions.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyChatMigrations } from "../src/migrations";
import { createDrizzleReactionStore } from "../src/reactions";
import { dbGate } from "../../../scripts/e2e/db-gate";
Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/read-state.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyChatMigrations } from "../src/migrations";
import { createDrizzleChatStore } from "../src/store";
import { dbGate } from "../../../scripts/e2e/db-gate";
Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/room-activity.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyChatMigrations } from "../src/migrations";
import { createDrizzleRoomMessageStore } from "../src/room-messages";
import { dbGate } from "../../../scripts/e2e/db-gate";
Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/settings-participants.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyChatMigrations } from "../src/migrations";
import { addParticipant } from "../src/participants";
import { createDrizzleChatStore } from "../src/store";
Expand Down
2 changes: 1 addition & 1 deletion packages/chat/test/settings-patch.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import { e2eDatabaseUrl } from "../../../scripts/e2e/harness";
import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url";
import { applyChatMigrations } from "../src/migrations";
import { createDrizzleChatStore } from "../src/store";
import { dbGate } from "../../../scripts/e2e/db-gate";
Expand Down
Loading
Loading