mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
style: apply the formatter across the tree
The formatter had never been run over these files, so `oxfmt` on a couple of touched files rewrote two dozen others and buried the real change. Running it everywhere once makes the next diff mean something. No behaviour changes: import ordering, line joining, and reindented JSON in the generated migration snapshots. Both test suites and both typechecks give the same answers as before, including the two type errors this does not fix.
This commit is contained in:
@@ -281,4 +281,3 @@ Route handler
|
|||||||
│
|
│
|
||||||
└─ returns normally ─────► c.json({ data: … })
|
└─ returns normally ─────► c.json({ data: … })
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Actor } from '@nestri/core/actor';
|
|||||||
import { Env } from '@nestri/core/env';
|
import { Env } from '@nestri/core/env';
|
||||||
import { ErrorCodes, VisibleError } from '@nestri/core/error';
|
import { ErrorCodes, VisibleError } from '@nestri/core/error';
|
||||||
import { Examples } from '@nestri/core/examples';
|
import { Examples } from '@nestri/core/examples';
|
||||||
import { User } from '@nestri/core/user/index';
|
|
||||||
import { Fingerprint } from '@nestri/core/user/fingerprint';
|
import { Fingerprint } from '@nestri/core/user/fingerprint';
|
||||||
|
import { User } from '@nestri/core/user/index';
|
||||||
import { VERIFICATION_TTL_MINUTES, Verification } from '@nestri/core/user/verification';
|
import { VERIFICATION_TTL_MINUTES, Verification } from '@nestri/core/user/verification';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { describeRoute } from 'hono-openapi';
|
import { describeRoute } from 'hono-openapi';
|
||||||
@@ -159,9 +159,7 @@ export namespace UserApi {
|
|||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
schema: Result(
|
schema: Result(z.object({ verified: z.boolean() }))
|
||||||
z.object({ verified: z.boolean() })
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
description: 'Email verified'
|
description: 'Email verified'
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ serialization lives here. The API and auth workers are thin pass-through transla
|
|||||||
|
|
||||||
## What it contains
|
## What it contains
|
||||||
|
|
||||||
| Area | Files | Purpose |
|
| Area | Files | Purpose |
|
||||||
| ---- | ----- | ------- |
|
| -------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
| **db** | `db/index.ts`, `db/types.ts`, `db/test.ts` | Drizzle + Postgres (`Database.use/transaction`), ULID column helpers |
|
| **db** | `db/index.ts`, `db/types.ts`, `db/test.ts` | Drizzle + Postgres (`Database.use/transaction`), ULID column helpers |
|
||||||
| **users** | `user/*` | Users, linked accounts, fingerprints, library |
|
| **users** | `user/*` | Users, linked accounts, fingerprints, library |
|
||||||
| **teams** | `team/*` | Teams + membership with roles (`team_member`) |
|
| **teams** | `team/*` | Teams + membership with roles (`team_member`) |
|
||||||
| **games** | `game/*` | Game catalog, depot content, per-host downloads |
|
| **games** | `game/*` | Game catalog, depot content, per-host downloads |
|
||||||
| **steam** | `steam/index.ts` | Steam API integration & SSH identity resolution |
|
| **steam** | `steam/index.ts` | Steam API integration & SSH identity resolution |
|
||||||
| **auth** | `auth/subjects.ts` | JWT subjects shared with the auth worker |
|
| **auth** | `auth/subjects.ts` | JWT subjects shared with the auth worker |
|
||||||
| **infra** | `env.ts`, `context.ts`, `actor.ts`, `fn.ts`, `id.ts`, `error.ts`, `examples.ts` | Environment, Actor model, zod-typed `fn()` wrappers, IDs, error types, examples |
|
| **infra** | `env.ts`, `context.ts`, `actor.ts`, `fn.ts`, `id.ts`, `error.ts`, `examples.ts` | Environment, Actor model, zod-typed `fn()` wrappers, IDs, error types, examples |
|
||||||
| **migrations** | `migrations/` | Drizzle-kit SQL migrations for Postgres schema |
|
| **migrations** | `migrations/` | Drizzle-kit SQL migrations for Postgres schema |
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,111 +1,111 @@
|
|||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1784801002476,
|
"when": 1784801002476,
|
||||||
"tag": "0000_quick_dark_phoenix",
|
"tag": "0000_quick_dark_phoenix",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1785312635128,
|
"when": 1785312635128,
|
||||||
"tag": "0001_opposite_senator_kelly",
|
"tag": "0001_opposite_senator_kelly",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 2,
|
"idx": 2,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1785379712946,
|
"when": 1785379712946,
|
||||||
"tag": "0002_light_mesmero",
|
"tag": "0002_light_mesmero",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 3,
|
"idx": 3,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1785382013687,
|
"when": 1785382013687,
|
||||||
"tag": "0003_many_pyro",
|
"tag": "0003_many_pyro",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 4,
|
"idx": 4,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1785588097470,
|
"when": 1785588097470,
|
||||||
"tag": "0004_remove_user_download_add_game_download",
|
"tag": "0004_remove_user_download_add_game_download",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 5,
|
"idx": 5,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1785909838801,
|
"when": 1785909838801,
|
||||||
"tag": "0005_flaky_may_parker",
|
"tag": "0005_flaky_may_parker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 6,
|
"idx": 6,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1786205230097,
|
"when": 1786205230097,
|
||||||
"tag": "0006_waitlist_verification_game_aliases",
|
"tag": "0006_waitlist_verification_game_aliases",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 7,
|
"idx": 7,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1788460224524,
|
"when": 1788460224524,
|
||||||
"tag": "0007_box_session_team_notnull",
|
"tag": "0007_box_session_team_notnull",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 8,
|
"idx": 8,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1788547836146,
|
"when": 1788547836146,
|
||||||
"tag": "0008_session_one_active_run_per_box",
|
"tag": "0008_session_one_active_run_per_box",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 9,
|
"idx": 9,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1788555252186,
|
"when": 1788555252186,
|
||||||
"tag": "0009_email_is_the_root_identity",
|
"tag": "0009_email_is_the_root_identity",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 10,
|
"idx": 10,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1788590292860,
|
"when": 1788590292860,
|
||||||
"tag": "0010_device_authorization_grant",
|
"tag": "0010_device_authorization_grant",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 11,
|
"idx": 11,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1788607804606,
|
"when": 1788607804606,
|
||||||
"tag": "0011_auth_state_in_postgres",
|
"tag": "0011_auth_state_in_postgres",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 12,
|
"idx": 12,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1788691753961,
|
"when": 1788691753961,
|
||||||
"tag": "0012_steam_enrolment_without_a_token",
|
"tag": "0012_steam_enrolment_without_a_token",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 13,
|
"idx": 13,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1788725541386,
|
"when": 1788725541386,
|
||||||
"tag": "0013_machine_endpoint_id",
|
"tag": "0013_machine_endpoint_id",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 14,
|
"idx": 14,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1789680491539,
|
"when": 1789680491539,
|
||||||
"tag": "0014_machine_public_label",
|
"tag": "0014_machine_public_label",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,9 @@ const out = path.join(here, '..', 'src', 'migrations.generated.ts');
|
|||||||
|
|
||||||
type JournalEntry = { idx: number; when: number; tag: string; breakpoints: boolean };
|
type JournalEntry = { idx: number; when: number; tag: string; breakpoints: boolean };
|
||||||
|
|
||||||
const journal = JSON.parse(
|
const journal = JSON.parse(fs.readFileSync(path.join(folder, 'meta', '_journal.json'), 'utf8')) as {
|
||||||
fs.readFileSync(path.join(folder, 'meta', '_journal.json'), 'utf8')
|
entries: JournalEntry[];
|
||||||
) as { entries: JournalEntry[] };
|
};
|
||||||
|
|
||||||
// Deliberately identical to drizzle-orm's own `readMigrationFiles`: the same
|
// Deliberately identical to drizzle-orm's own `readMigrationFiles`: the same
|
||||||
// `--> statement-breakpoint` split, the same sha256 over the whole file, the
|
// `--> statement-breakpoint` split, the same sha256 over the whole file, the
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ describe('PostgresCodeStore', () => {
|
|||||||
|
|
||||||
await store.create(hash(), record(), 60);
|
await store.create(hash(), record(), 60);
|
||||||
|
|
||||||
const [row] = await sql`select count(*)::int as n from authorization_code where code_hash = ${stale}`;
|
const [row] =
|
||||||
|
await sql`select count(*)::int as n from authorization_code where code_hash = ${stale}`;
|
||||||
expect(row!.n).toBe(0);
|
expect(row!.n).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { afterAll, describe, expect, test } from 'bun:test';
|
|||||||
import { Fixtures } from '../db/fixtures.js';
|
import { Fixtures } from '../db/fixtures.js';
|
||||||
import { testDb } from '../db/test.js';
|
import { testDb } from '../db/test.js';
|
||||||
import { Identifier } from '../id.js';
|
import { Identifier } from '../id.js';
|
||||||
import { Placement } from './placement.js';
|
|
||||||
import { Box } from './index.js';
|
import { Box } from './index.js';
|
||||||
|
import { Placement } from './placement.js';
|
||||||
|
|
||||||
const sql = testDb();
|
const sql = testDb();
|
||||||
|
|
||||||
@@ -46,9 +46,7 @@ describe('Placement', () => {
|
|||||||
|
|
||||||
// `box.machineId` is notNull, so a placer with no candidate must refuse
|
// `box.machineId` is notNull, so a placer with no candidate must refuse
|
||||||
// rather than hand back something the insert would reject.
|
// rather than hand back something the insert would reject.
|
||||||
await expect(
|
await expect(Placement.choose({ userId: owner.userId, tier: 'sm' })).rejects.toThrow();
|
||||||
Placement.choose({ userId: owner.userId, tier: 'sm' })
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('more than one candidate is refused rather than picked silently', async () => {
|
test('more than one candidate is refused rather than picked silently', async () => {
|
||||||
@@ -59,9 +57,7 @@ describe('Placement', () => {
|
|||||||
// There is no policy for choosing between hosts yet. Inventing one here
|
// There is no policy for choosing between hosts yet. Inventing one here
|
||||||
// is how a placement decision ends up buried in the caller: the refusal
|
// is how a placement decision ends up buried in the caller: the refusal
|
||||||
// is what keeps the choice in one replaceable place.
|
// is what keeps the choice in one replaceable place.
|
||||||
await expect(
|
await expect(Placement.choose({ userId: owner.userId, tier: 'sm' })).rejects.toThrow();
|
||||||
Placement.choose({ userId: owner.userId, tier: 'sm' })
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the placer is swappable without touching box creation', async () => {
|
test('the placer is swappable without touching box creation', async () => {
|
||||||
|
|||||||
@@ -127,10 +127,7 @@ export namespace Database {
|
|||||||
// callback ran on — harmless by luck, since neither was a real
|
// callback ran on — harmless by luck, since neither was a real
|
||||||
// transaction, and twice the pools either way.
|
// transaction, and twice the pools either way.
|
||||||
const db = client();
|
const db = client();
|
||||||
const result = await TransactionContext.provide(
|
const result = await TransactionContext.provide({ effects, tx: db }, () => callback(db));
|
||||||
{ effects, tx: db },
|
|
||||||
() => callback(db)
|
|
||||||
);
|
|
||||||
await Promise.all(effects.map((x) => x()));
|
await Promise.all(effects.map((x) => x()));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,13 @@ describe('Session claim', () => {
|
|||||||
test('re-reporting the state you already reported changes nothing', async () => {
|
test('re-reporting the state you already reported changes nothing', async () => {
|
||||||
const { machineId, session } = await requested('ses-repeat', 5424);
|
const { machineId, session } = await requested('ses-repeat', 5424);
|
||||||
|
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
const again = await Session.transition({
|
const again = await Session.transition({
|
||||||
claimToken: HOLDER,
|
claimToken: HOLDER,
|
||||||
id: session.id,
|
id: session.id,
|
||||||
@@ -373,8 +379,20 @@ describe('Session claim', () => {
|
|||||||
expect(skipped.outcome).toBe('illegal');
|
expect(skipped.outcome).toBe('illegal');
|
||||||
expect((await Session.fromID(session.id))?.state).toBe('requested');
|
expect((await Session.fromID(session.id))?.state).toBe('requested');
|
||||||
|
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'failed', errorMessage: 'no' });
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'failed',
|
||||||
|
errorMessage: 'no'
|
||||||
|
});
|
||||||
|
|
||||||
// Terminal is terminal: a dead session cannot be resurrected.
|
// Terminal is terminal: a dead session cannot be resurrected.
|
||||||
const raised = await Session.transition({
|
const raised = await Session.transition({
|
||||||
@@ -390,7 +408,13 @@ describe('Session claim', () => {
|
|||||||
|
|
||||||
test('the timestamps survive a duplicate report, which is what billing rests on', async () => {
|
test('the timestamps survive a duplicate report, which is what billing rests on', async () => {
|
||||||
const { machineId, session } = await requested('ses-idempotent', 5426);
|
const { machineId, session } = await requested('ses-idempotent', 5426);
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
const live = await Session.transition({
|
const live = await Session.transition({
|
||||||
claimToken: HOLDER,
|
claimToken: HOLDER,
|
||||||
id: session.id,
|
id: session.id,
|
||||||
@@ -414,7 +438,13 @@ describe('Session claim', () => {
|
|||||||
const { machineId, session } = await requested('ses-ticket-scope', 5427);
|
const { machineId, session } = await requested('ses-ticket-scope', 5427);
|
||||||
const other = await scene('ses-ticket-other', 5428);
|
const other = await scene('ses-ticket-other', 5428);
|
||||||
|
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
|
||||||
const refused = await Session.publishTicket({
|
const refused = await Session.publishTicket({
|
||||||
claimToken: HOLDER,
|
claimToken: HOLDER,
|
||||||
@@ -426,12 +456,22 @@ describe('Session claim', () => {
|
|||||||
expect((await Session.fromID(session.id))?.ticket).toBeNull();
|
expect((await Session.fromID(session.id))?.ticket).toBeNull();
|
||||||
|
|
||||||
// A ticket may appear while the state is still `starting`.
|
// A ticket may appear while the state is still `starting`.
|
||||||
const first = await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'one' });
|
const first = await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'one'
|
||||||
|
});
|
||||||
expect(first.outcome).toBe('published');
|
expect(first.outcome).toBe('published');
|
||||||
expect(first.session?.ticket).toBe('one');
|
expect(first.session?.ticket).toBe('one');
|
||||||
expect(first.session?.state).toBe('starting');
|
expect(first.session?.state).toBe('starting');
|
||||||
|
|
||||||
const second = await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'two' });
|
const second = await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'two'
|
||||||
|
});
|
||||||
expect(second.session?.ticket).toBe('two');
|
expect(second.session?.ticket).toBe('two');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -691,11 +731,34 @@ describe('Session claim', () => {
|
|||||||
|
|
||||||
test('a stopped session has no address to publish', async () => {
|
test('a stopped session has no address to publish', async () => {
|
||||||
const { machineId, session } = await requested('ses-ticket-dead', 5429);
|
const { machineId, session } = await requested('ses-ticket-dead', 5429);
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'live', errorMessage: null });
|
claimToken: HOLDER,
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'ended', errorMessage: null });
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'live',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'ended',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
|
||||||
const result = await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'late' });
|
const result = await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'late'
|
||||||
|
});
|
||||||
expect(result.outcome).toBe('closed');
|
expect(result.outcome).toBe('closed');
|
||||||
expect((await Session.fromID(session.id))?.ticket).toBeNull();
|
expect((await Session.fromID(session.id))?.ticket).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -739,9 +802,27 @@ describe('Session one active run per box', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const first = await mk();
|
const first = await mk();
|
||||||
await Session.transition({ claimToken: HOLDER, id: first.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
await Session.transition({ claimToken: HOLDER, id: first.id, machineId, state: 'live', errorMessage: null });
|
claimToken: HOLDER,
|
||||||
await Session.transition({ claimToken: HOLDER, id: first.id, machineId, state: 'ended', errorMessage: null });
|
id: first.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: first.id,
|
||||||
|
machineId,
|
||||||
|
state: 'live',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: first.id,
|
||||||
|
machineId,
|
||||||
|
state: 'ended',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
|
||||||
// The index is partial for exactly this reason: a box is a durable
|
// The index is partial for exactly this reason: a box is a durable
|
||||||
// thing and playing twice is the ordinary case, so a stopped run must
|
// thing and playing twice is the ordinary case, so a stopped run must
|
||||||
@@ -755,11 +836,29 @@ describe('Session one active run per box', () => {
|
|||||||
describe('Session tickets and the end of a run', () => {
|
describe('Session tickets and the end of a run', () => {
|
||||||
test('stopping a run takes its address away', async () => {
|
test('stopping a run takes its address away', async () => {
|
||||||
const { machineId, session } = await requestedRun('ses-ticket-cleared', 5452);
|
const { machineId, session } = await requestedRun('ses-ticket-cleared', 5452);
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'live', errorMessage: null });
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'live',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
(await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'live-address' })).session
|
(
|
||||||
?.ticket
|
await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'live-address'
|
||||||
|
})
|
||||||
|
).session?.ticket
|
||||||
).toBe('live-address');
|
).toBe('live-address');
|
||||||
|
|
||||||
const ended = await Session.transition({
|
const ended = await Session.transition({
|
||||||
@@ -779,8 +878,19 @@ describe('Session tickets and the end of a run', () => {
|
|||||||
|
|
||||||
test('a run that failed does not keep an address either', async () => {
|
test('a run that failed does not keep an address either', async () => {
|
||||||
const { machineId, session } = await requestedRun('ses-ticket-cleared-fail', 5453);
|
const { machineId, session } = await requestedRun('ses-ticket-cleared-fail', 5453);
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'starting-address' });
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'starting-address'
|
||||||
|
});
|
||||||
|
|
||||||
const failed = await Session.transition({
|
const failed = await Session.transition({
|
||||||
claimToken: HOLDER,
|
claimToken: HOLDER,
|
||||||
@@ -814,12 +924,24 @@ describe('Session and the box underneath it', () => {
|
|||||||
// `created` while a run on it was `live`.
|
// `created` while a run on it was `live`.
|
||||||
expect((await Box.fromID(box.id))?.state).toBe('created');
|
expect((await Box.fromID(box.id))?.state).toBe('created');
|
||||||
|
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
// `starting` is deliberately not a box state: that transition is
|
// `starting` is deliberately not a box state: that transition is
|
||||||
// synchronous from the agent's side, so nothing would ever write it.
|
// synchronous from the agent's side, so nothing would ever write it.
|
||||||
expect((await Box.fromID(box.id))?.state).toBe('created');
|
expect((await Box.fromID(box.id))?.state).toBe('created');
|
||||||
|
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'live', errorMessage: null });
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'live',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
const running = await Box.fromID(box.id);
|
const running = await Box.fromID(box.id);
|
||||||
expect(running?.state).toBe('running');
|
expect(running?.state).toBe('running');
|
||||||
expect(running?.stopReason).toBeNull();
|
expect(running?.stopReason).toBeNull();
|
||||||
@@ -828,9 +950,27 @@ describe('Session and the box underneath it', () => {
|
|||||||
|
|
||||||
test('a run that ends stops its box, cleanly', async () => {
|
test('a run that ends stops its box, cleanly', async () => {
|
||||||
const { machineId, box, session } = await requestedRun('ses-box-ended', 5461);
|
const { machineId, box, session } = await requestedRun('ses-box-ended', 5461);
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'live', errorMessage: null });
|
claimToken: HOLDER,
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'ended', errorMessage: null });
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'live',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'ended',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
|
||||||
const stopped = await Box.fromID(box.id);
|
const stopped = await Box.fromID(box.id);
|
||||||
expect(stopped?.state).toBe('stopped');
|
expect(stopped?.state).toBe('stopped');
|
||||||
@@ -840,7 +980,13 @@ describe('Session and the box underneath it', () => {
|
|||||||
|
|
||||||
test('a run that fails stops its box in the words the agent used', async () => {
|
test('a run that fails stops its box in the words the agent used', async () => {
|
||||||
const { machineId, box, session } = await requestedRun('ses-box-failed', 5462);
|
const { machineId, box, session } = await requestedRun('ses-box-failed', 5462);
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
await Session.transition({
|
await Session.transition({
|
||||||
claimToken: HOLDER,
|
claimToken: HOLDER,
|
||||||
id: session.id,
|
id: session.id,
|
||||||
@@ -891,24 +1037,68 @@ describe('Session tickets need a claim first', () => {
|
|||||||
// A ticket is the address of something being brought up, so publishing
|
// A ticket is the address of something being brought up, so publishing
|
||||||
// one for a `requested` run means the agent skipped the claim — the
|
// one for a `requested` run means the agent skipped the claim — the
|
||||||
// step that is the only mutual exclusion in the design.
|
// step that is the only mutual exclusion in the design.
|
||||||
const early = await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'too-soon' });
|
const early = await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'too-soon'
|
||||||
|
});
|
||||||
expect(early.outcome).toBe('unclaimed');
|
expect(early.outcome).toBe('unclaimed');
|
||||||
expect((await Session.fromID(session.id))?.ticket).toBeNull();
|
expect((await Session.fromID(session.id))?.ticket).toBeNull();
|
||||||
|
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
const now = await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'in-time' });
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
const now = await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'in-time'
|
||||||
|
});
|
||||||
expect(now.outcome).toBe('published');
|
expect(now.outcome).toBe('published');
|
||||||
expect(now.session?.ticket).toBe('in-time');
|
expect(now.session?.ticket).toBe('in-time');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the two refusals are different answers, because they are different mistakes', async () => {
|
test('the two refusals are different answers, because they are different mistakes', async () => {
|
||||||
const { machineId, session } = await requestedRun('ses-ticket-refusals', 5466);
|
const { machineId, session } = await requestedRun('ses-ticket-refusals', 5466);
|
||||||
const unclaimed = await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'a' });
|
const unclaimed = await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'a'
|
||||||
|
});
|
||||||
|
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'starting', errorMessage: null });
|
await Session.transition({
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'live', errorMessage: null });
|
claimToken: HOLDER,
|
||||||
await Session.transition({ claimToken: HOLDER, id: session.id, machineId, state: 'ended', errorMessage: null });
|
id: session.id,
|
||||||
const closed = await Session.publishTicket({ claimToken: HOLDER, id: session.id, machineId, ticket: 'b' });
|
machineId,
|
||||||
|
state: 'starting',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'live',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
await Session.transition({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
state: 'ended',
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
const closed = await Session.publishTicket({
|
||||||
|
claimToken: HOLDER,
|
||||||
|
id: session.id,
|
||||||
|
machineId,
|
||||||
|
ticket: 'b'
|
||||||
|
});
|
||||||
|
|
||||||
// One is an agent that has not claimed the work; the other is a run
|
// One is an agent that has not claimed the work; the other is a run
|
||||||
// with nothing left to reach. Collapsing them would tell an agent
|
// with nothing left to reach. Collapsing them would tell an agent
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Actor } from '../actor.js';
|
|||||||
import { Fixtures } from '../db/fixtures.js';
|
import { Fixtures } from '../db/fixtures.js';
|
||||||
import { testDb } from '../db/test.js';
|
import { testDb } from '../db/test.js';
|
||||||
import { Identifier } from '../id.js';
|
import { Identifier } from '../id.js';
|
||||||
import { Member } from './member.js';
|
|
||||||
import { Team } from './index.js';
|
import { Team } from './index.js';
|
||||||
|
import { Member } from './member.js';
|
||||||
|
|
||||||
const sql = testDb();
|
const sql = testDb();
|
||||||
|
|
||||||
@@ -44,7 +44,10 @@ describe('Team.ensurePersonal', () => {
|
|||||||
// the user is created — that is what backfills accounts made before the
|
// the user is created — that is what backfills accounts made before the
|
||||||
// call existed. A second call must not mint a second team.
|
// call existed. A second call must not mint a second team.
|
||||||
const again = await Actor.with(
|
const again = await Actor.with(
|
||||||
{ type: 'user', properties: { userID: owner.userId, linkedAccountID: owner.linkedAccountId } },
|
{
|
||||||
|
type: 'user',
|
||||||
|
properties: { userID: owner.userId, linkedAccountID: owner.linkedAccountId }
|
||||||
|
},
|
||||||
() => Team.ensurePersonal({ displayName: 'team-idempotent' })
|
() => Team.ensurePersonal({ displayName: 'team-idempotent' })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { ErrorCodes, VisibleError } from '../error.js';
|
|||||||
import { fn } from '../fn.js';
|
import { fn } from '../fn.js';
|
||||||
import { Identifier } from '../id.js';
|
import { Identifier } from '../id.js';
|
||||||
import { User } from './index.js';
|
import { User } from './index.js';
|
||||||
import { UserTable } from './user.sql.js';
|
|
||||||
import { LinkedAccount } from './linked-account.js';
|
import { LinkedAccount } from './linked-account.js';
|
||||||
import { LinkedAccountTable } from './linked-account.sql.js';
|
import { LinkedAccountTable } from './linked-account.sql.js';
|
||||||
|
import { UserTable } from './user.sql.js';
|
||||||
|
|
||||||
const STEAM_ID_RE = /^\d{17}$/;
|
const STEAM_ID_RE = /^\d{17}$/;
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ export namespace Waitlist {
|
|||||||
return tx
|
return tx
|
||||||
.select()
|
.select()
|
||||||
.from(WaitlistEntryTable)
|
.from(WaitlistEntryTable)
|
||||||
.where(and(eq(WaitlistEntryTable.email, input.email), isNull(WaitlistEntryTable.timeDeleted)))
|
.where(
|
||||||
|
and(eq(WaitlistEntryTable.email, input.email), isNull(WaitlistEntryTable.timeDeleted))
|
||||||
|
)
|
||||||
.then((rows) => rows.at(0) ?? null);
|
.then((rows) => rows.at(0) ?? null);
|
||||||
});
|
});
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|||||||
Reference in New Issue
Block a user