wren/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files e45e4989a79ef3f801c453528dd5b471f91bea4f
126 lines · 4.5 KB · ~1.3k tok Raw History
1
/**
2
 * Every door answers as the table says. The test acts as each identity the
3
 * contract declares — on a drawing that names them and on one that names
4
 * someone else — and calls the real actions. A rule written by hand in an
5
 * action, or a cell changed without its door, fails here.
6
 */
7
import { expect, test } from "bun:test";
8
import { mkdtempSync } from "node:fs";
9
import { tmpdir } from "node:os";
10
import { join } from "node:path";
11
import {
12
  deleteDrawing,
13
  getAudit,
14
  getDrawing,
15
  listAudit,
16
  listDrawings,
17
  startDrawing,
18
} from "./actions.ts";
19
import { record } from "./audit.ts";
20
import { actingAs, identities, table, type Verb } from "./can.ts";
21
import { contract } from "./contract.ts";
22
import { openDb } from "./db.ts";
23
import { setDrawer } from "./draw.ts";
24
import type { Who } from "./who.ts";
25
26
const db = openDb(mkdtempSync(join(tmpdir(), "app-contract-")));
27
28
/** The identity the evaluator acts as, as the forge would have signed it. */
29
function whoIs(identity: string): Who {
30
  const { actor } = actingAs(contract, identity);
31
  const role = actor.roles[0];
32
  if (actor.id === null || (role !== "user" && role !== "admin"))
33
    return { role: "anon" };
34
  return { role, id: actor.id, name: identity };
35
}
36
37
// Drawings finish at once here; a row made by hand is already done.
38
setDrawer(async () => []);
39
const drawingBy = (authorId: string): number =>
40
  db
41
    .query<
42
      { id: number },
43
      [string]
44
    >("INSERT INTO drawing (prompt, kind, status, author_id, author_name) VALUES ('p', 'mark', 'done', ?1, ?1) RETURNING id")
45
    .get(authorId)!.id;
46
/** Each create is a new person, so "one at a time" never answers for the table. */
47
let fresh = 0;
48
49
const auditRow = (): number => {
50
  record(db, {
51
    who: { role: "anon" },
52
    resource: "drawing",
53
    verb: "create",
54
    outcome: "signin_required",
55
  });
56
  return db.query<{ id: number }, []>("SELECT max(id) AS id FROM audit").get()!
57
    .id;
58
};
59
60
/** One door per verb an action serves: did it let `who` through, on `row`? */
61
const doors: Record<
62
  string,
63
  Partial<Record<Verb, (who: Who, authorId: string) => boolean>>
64
> = {
65
  drawing: {
66
    create: (who) => {
67
      const made = startDrawing(db, who.role === "anon" ? who : { ...who, id: `${who.id}-${++fresh}` }, { prompt: "hello" });
68
      // Settled at once, so the cap on drawings in flight never answers for the table.
69
      db.exec("UPDATE drawing SET status = 'done' WHERE status = 'drawing'");
70
      return made.ok;
71
    },
72
    list: (who) => listDrawings(db, who).ok,
73
    read: (who, authorId) => getDrawing(db, who, { id: drawingBy(authorId) }).ok,
74
    // There is no action that changes a drawing: Claude's answer is kept as it came.
75
    update: () => false,
76
    delete: (who, authorId) => deleteDrawing(db, who, { id: drawingBy(authorId) }).ok,
77
  },
78
  // The trail is written by the app itself, never through a door: there
79
  // is no action that creates, changes or removes a row of it.
80
  audit: {
81
    list: (who) => listAudit(db, who).ok,
82
    read: (who) => getAudit(db, who, { id: auditRow() }).ok,
83
    create: () => false,
84
    update: () => false,
85
    delete: () => false,
86
  },
87
};
88
89
test("the contract names the identities the forge signs", () => {
90
  expect(identities(contract).sort()).toEqual(["admin", "anon", "user"]);
91
});
92
93
test("every resource in the contract has every door here", () => {
94
  expect(Object.keys(doors).sort()).toEqual(
95
    Object.keys(contract.resources).sort(),
96
  );
97
  for (const resource of Object.keys(contract.resources))
98
    expect(Object.keys(doors[resource] ?? {}).sort()).toEqual([
99
      "create",
100
      "delete",
101
      "list",
102
      "read",
103
      "update",
104
    ]);
105
});
106
107
test("every door answers as the table says, as every identity", () => {
108
  let asked = 0;
109
  for (const cell of table(contract)) {
110
    const door = doors[cell.resource]?.[cell.verb];
111
    if (door === undefined) continue;
112
    const who = whoIs(cell.as);
113
    const { mine, theirs } = actingAs(contract, cell.as);
114
    const onMine = door(who, String(mine["author_id"]));
115
    const onTheirs = door(who, String(theirs["author_id"]));
116
    const said = `${cell.resource}.${cell.verb} as ${cell.as}`;
117
    asked++;
118
    if (cell.answer === "yes")
119
      expect([said, onMine, onTheirs]).toEqual([said, true, true]);
120
    else if (cell.answer === "no")
121
      expect([said, onMine, onTheirs]).toEqual([said, false, false]);
122
    else if (cell.verb === "list") expect([said, onMine]).toEqual([said, true]);
123
    else expect([said, onMine, onTheirs]).toEqual([said, true, false]);
124
  }
125
  expect(asked).toBeGreaterThan(0);
126
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts