The Coline App SDK: apps, tools, file types and surfaces that run inside Coline workspaces, and public websites with Coline Sites. TypeScript, with an in-memory test workspace. MIT.
| 5 | it("validates manifest configuration", () => { |
| 6 | const manifest = colineAppManifestSchema.parse({ |
| 7 | key: "com.coline.notes", |
| 8 | name: "Notes", |
| 9 | permissions: ["app.home.read", "files.write", "index.write"], |
| 10 | kairo: { |
| 11 | description: "Notes documents can be searched by Kairo.", |
| 12 | documentTypes: [ |
| 13 | { |
| 14 | documentType: "note", |
| 15 | name: "Note", |
| 16 | metadata: [{ key: "tags", description: "Note tags" }], |
| 17 | }, |
| 18 | ], |
| 19 | }, |
| 20 | notificationChannels: [ |
| 21 | { |
| 22 | key: "updates", |
| 23 | name: "Updates", |
| 24 | defaultDeliveries: ["in_app", "desktop"], |
| 25 | }, |
| 26 | ], |
| 27 | hosting: { |
| 28 | mode: "external", |
| 29 | baseUrl: "https://notes.example.com", |
| 30 | }, |
| 31 | }); |
| 32 |
| 33 | expect(manifest.hosting.baseUrl).toBe("https://notes.example.com"); |
| 34 | expect(manifest.kairo?.documentTypes[0]?.documentType).toBe("note"); |
| 35 | }); |
| 36 |
| 37 | it("registers file types and handlers in the app definition", () => { |
| 38 | const app = new ColineApp({ |
| 39 | key: "com.coline.notes", |
| 40 | name: "Notes", |
| 41 | permissions: ["app.home.read", "files.read", "files.write"], |
| 42 | hosting: { |
| 43 | mode: "external", |
| 44 | baseUrl: "https://notes.example.com", |
| 45 | }, |
| 46 | }) |
| 47 | .defineFileType({ |
| 48 | typeKey: "com.coline.notes.note", |
| 49 | name: "Note", |
| 50 | storage: "coline_document", |
| 51 | }) |
| 52 | .defineNotificationChannel({ |
| 53 | key: "updates", |
| 54 | name: "Updates", |
| 55 | defaultDeliveries: ["in_app", "desktop"], |
| 56 | }) |
| 57 | .onHomeRender(async () => |
| 58 | ui.stack([ |
| 59 | ui.heading("Notes"), |
| 60 | ui.button("New note", { |
| 61 | action: { type: "create_note" }, |
| 62 | }), |
| 63 | ]), |
| 64 | ) |
| 65 | .onIndexSync(() => []); |
| 66 |
| 67 | const definition = app.getDefinition(); |
| 68 |
| 69 | expect(definition.fileTypes).toHaveLength(1); |
| 70 | expect(definition.notificationChannels).toHaveLength(1); |
| 71 | expect(definition.fileTypes[0]?.typeKey).toBe("com.coline.notes.note"); |
| 72 | expect(typeof definition.handlers.homeRender).toBe("function"); |
| 73 | expect(typeof definition.handlers.indexSync).toBe("function"); |
| 74 | }); |
| 75 |
| 76 | it("builds a typed registration payload for API submission", () => { |
| 77 | const app = new ColineApp({ |
| 78 | key: "com.coline.notes", |
| 79 | name: "Notes", |
| 80 | permissions: ["app.home.read"], |
| 81 | hosting: { |
| 82 | mode: "external", |
| 83 | baseUrl: "https://notes.example.com", |
| 84 | }, |
| 85 | }) |
| 86 | .defineFileType({ |
| 87 | typeKey: "com.coline.notes.note", |
| 88 | name: "Note", |
| 89 | storage: "coline_document", |
| 90 | }) |
| 91 | .defineNotificationChannel({ |
| 92 | key: "updates", |
| 93 | name: "Updates", |
| 94 | defaultDeliveries: ["in_app"], |
| 95 | }); |
| 96 |
| 97 | const payload = app.getRegistrationPayload("0.1.0"); |
| 98 |
| 99 | expect(payload.version).toBe("0.1.0"); |
| 100 | expect(payload.manifest.key).toBe("com.coline.notes"); |
| 101 | expect(payload.fileTypes[0]?.homeRenderMode).toBe("cached"); |
| 102 | expect(payload.manifest.notificationChannels[0]?.key).toBe("updates"); |
| 103 | }); |
| 104 |
| 105 | it("builds typed search and Kairo configuration", () => { |
| 106 | const app = new ColineApp({ |
| 107 | key: "com.coline.notes", |
| 108 | name: "Notes", |
| 109 | permissions: ["index.write"], |
| 110 | hosting: { |
| 111 | mode: "external", |
| 112 | baseUrl: "https://notes.example.com", |
| 113 | }, |
| 114 | }).defineKairo({ |
| 115 | description: "Indexed notes for Kairo.", |
| 116 | documentTypes: [ |
| 117 | { |
| 118 | documentType: "note", |
| 119 | name: "Note", |
| 120 | instructions: "Read note bodies as canonical content.", |
| 121 | metadata: [{ key: "tags" }], |
| 122 | }, |
| 123 | ], |
| 124 | }); |
| 125 |
| 126 | const document = app.search.document({ |
| 127 | documentKey: "note:file_123", |
| 128 | documentType: "note", |
| 129 | fileId: "file_123", |
| 130 | title: "Roadmap", |
| 131 | body: "Quarterly roadmap", |
| 132 | metadata: { tags: ["planning"] }, |
| 133 | }); |
| 134 |
| 135 | expect(app.kairo?.documentTypes[0]?.name).toBe("Note"); |
| 136 | expect(document.documentType).toBe("note"); |
| 137 | expect(app.search.upsert({ documents: [document] }).documents).toHaveLength(1); |
| 138 | }); |
| 139 |
| 140 | it("rejects invalid external hosting urls", () => { |
| 141 | expect(() => { |
| 142 | new ColineApp({ |
| 143 | key: "com.coline.invalid", |
| 144 | name: "Invalid", |
| 145 | permissions: [], |
| 146 | hosting: { |
| 147 | mode: "external", |
| 148 | baseUrl: "not-a-url", |
| 149 | }, |
| 150 | }); |
| 151 | }).toThrow(); |
| 152 | }); |
| 153 |
| 154 | it("builds typed notification payloads", () => { |
| 155 | const app = new ColineApp({ |
| 156 | key: "com.coline.notes", |
| 157 | name: "Notes", |
| 158 | permissions: ["notifications.write"], |
| 159 | hosting: { |
| 160 | mode: "external", |
| 161 | baseUrl: "https://notes.example.com", |
| 162 | }, |
| 163 | }).defineNotificationChannel({ |
| 164 | key: "updates", |
| 165 | name: "Updates", |
| 166 | defaultDeliveries: ["in_app", "desktop"], |
| 167 | }); |
| 168 |
| 169 | const payload = app.createNotification({ |
| 170 | channelKey: "updates", |
| 171 | typeKey: "note.shared", |
| 172 | recipients: [{ userId: "user_123" }], |
| 173 | title: "A note was shared", |
| 174 | metadata: { noteId: "note_123" }, |
| 175 | }); |
| 176 |
| 177 | expect(payload.priority).toBe("default"); |
| 178 | expect(payload.recipients[0]?.userId).toBe("user_123"); |
| 179 | }); |
| 180 |
| 181 | it("builds typed ambient event batches", () => { |
| 182 | const app = new ColineApp({ |
| 183 | key: "com.coline.notes", |
| 184 | name: "Notes", |
| 185 | permissions: ["ambient.events.write"], |
| 186 | hosting: { |
| 187 | mode: "external", |
| 188 | baseUrl: "https://notes.example.com", |
| 189 | }, |
| 190 | }); |
| 191 |
| 192 | const payload = app.createAmbientEvents({ |
| 193 | events: [ |
| 194 | { |
| 195 | action: "synced", |
| 196 | entityType: "app_document", |
| 197 | entityId: "note_123", |
| 198 | dedupeKey: "sync:note_123:v1", |
| 199 | payload: { title: "Roadmap" }, |
| 200 | }, |
| 201 | ], |
| 202 | }); |
| 203 |
| 204 | expect(payload.events[0]?.sourceKind).toBe("app_signal"); |
| 205 | expect(payload.events[0]?.payload).toEqual({ title: "Roadmap" }); |
| 206 | }); |
| 207 | }); |
| 208 |