feat: add actions to FeedSource interface

Add listActions() and executeAction() to FeedSource for write
operations back to external services. Actions use arktype schemas
for input validation via StandardSchemaV1.

- ActionDefinition type with optional input schema
- FeedEngine routes actions with existence and ID validation
- Source IDs use reverse-domain format (aris.location, aris.tfl)
- LocationSource: update-location action with schema validation
- TflSource: set-lines-of-interest action with lineId validation
- No-op implementations for sources without actions

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-02-15 12:26:23 +00:00
parent 4d6cac7ec8
commit 699155e0d8
29 changed files with 1169 additions and 116 deletions

View File

@@ -16,7 +16,7 @@ describe("LocationSource", () => {
describe("FeedSource interface", () => {
test("has correct id", () => {
const source = new LocationSource()
expect(source.id).toBe("location")
expect(source.id).toBe("aris.location")
})
test("fetchItems always returns empty array", async () => {
@@ -147,4 +147,40 @@ describe("LocationSource", () => {
expect(listener2).toHaveBeenCalledTimes(1)
})
})
describe("actions", () => {
test("listActions returns update-location action", async () => {
const source = new LocationSource()
const actions = await source.listActions()
expect(actions["update-location"]).toBeDefined()
expect(actions["update-location"]!.id).toBe("update-location")
expect(actions["update-location"]!.input).toBeDefined()
})
test("executeAction update-location pushes location", async () => {
const source = new LocationSource()
expect(source.lastLocation).toBeNull()
const location = createLocation({ lat: 40.7128, lng: -74.006 })
await source.executeAction("update-location", location)
expect(source.lastLocation).toEqual(location)
})
test("executeAction throws on invalid input", async () => {
const source = new LocationSource()
await expect(
source.executeAction("update-location", { lat: "not a number" }),
).rejects.toThrow()
})
test("executeAction throws for unknown action", async () => {
const source = new LocationSource()
await expect(source.executeAction("nonexistent", {})).rejects.toThrow("Unknown action")
})
})
})