PranitInitial commit439f83c3 months agoHistory
207 lines · 6.9 KB · Zig
RawDownload
//! First-party lifecycle hook providers.//!//! The Herdr provider reports semantic foreground state for interactive work.//! Notification hooks live in `notifications` and keep sound policy outside//! the Core hook harness. const std = @import("std");const hooks = @import("../core/hooks/hooks.zig");const herdr = @import("hooks/herdr.zig"); pub const notifications = @import("hooks/notifications.zig");pub const Client = herdr.Client; pub fn Runtime(comptime App: type) type {    return struct {        /// Must run before the lifecycle runtime is frozen (currently the        /// notification runtime performs the sole freeze right after this).        pub fn configure(app: *App, active_session_id: ?[]const u8) !void {            app.herdr.initFromEnv(app.alloc);            if (!app.herdr.enabled) return;             if (active_session_id) |session_id| {                app.herdr.reportSession(session_id);            }            app.herdr.reportState(.idle, null);            app.herdr.announce();             try register(app);        }         fn register(app: *App) !void {            try app.lifecycle_runtime.registerPostTurnEnd(.{                .name = "fx.herdr.turn_end",                .ctx = app,                .run = postTurnEndHandler,            });            try app.lifecycle_runtime.registerAttentionRequired(.{                .name = "fx.herdr.attention_required",                .ctx = app,                .run = attentionRequiredHandler,            });        }         pub fn reportWorking(app: *App) void {            app.herdr.reportState(.working, null);        }         fn postTurnEndHandler(            raw: *anyopaque,            input: hooks.PostTurnEndInput,        ) hooks.HandlerError!void {            const app: *App = @ptrCast(@alignCast(raw));            if (input.invocation.scope.kind != .interactive) return;            app.herdr.reportState(.idle, null);        }         fn attentionRequiredHandler(            raw: *anyopaque,            input: hooks.AttentionRequiredInput,        ) hooks.HandlerError!void {            const app: *App = @ptrCast(@alignCast(raw));            if (input.invocation.scope.kind != .interactive) return;            app.herdr.reportState(.blocked, attentionStatus(input.kind));        }         fn attentionStatus(kind: hooks.AttentionKind) []const u8 {            return switch (kind) {                .permission => "permission",                .question => "question",                .route_recovery => "recovery",            };        }    };} const RecordingClient = struct {    const Report = struct {        state: herdr.State,        status: ?[]const u8,    };     reports: [6]Report = undefined,    report_count: usize = 0,    enabled: bool = false,    enable_on_init: bool = true,    initialized: bool = false,    announced: bool = false,    session_id: ?[]const u8 = null,     fn initFromEnv(self: *RecordingClient, _: std.mem.Allocator) void {        self.initialized = true;        self.enabled = self.enable_on_init;    }     fn reportSession(self: *RecordingClient, session_id: []const u8) void {        self.session_id = session_id;    }     fn announce(self: *RecordingClient) void {        self.announced = true;    }     fn reportState(self: *RecordingClient, state: herdr.State, status: ?[]const u8) void {        self.reports[self.report_count] = .{ .state = state, .status = status };        self.report_count += 1;    }}; test "built-in Herdr hooks report only interactive lifecycle state" {    const TestApp = struct {        alloc: std.mem.Allocator,        lifecycle_runtime: hooks.Runtime,        herdr: RecordingClient = .{},    };    const Provider = Runtime(TestApp);     var app = TestApp{        .alloc = std.testing.allocator,        .lifecycle_runtime = hooks.Runtime.init(std.testing.allocator),    };    defer app.lifecycle_runtime.deinit();     try Provider.configure(&app, "session-42");    const view = app.lifecycle_runtime.freeze();    try std.testing.expect(app.herdr.initialized);    try std.testing.expect(app.herdr.announced);    try std.testing.expectEqualStrings("session-42", app.herdr.session_id orelse return error.TestExpectedEqual);    try std.testing.expect(view.hasPostTurnEnd());    try std.testing.expect(view.hasAttentionRequired());     Provider.reportWorking(&app);    view.runPostTurnEnd(.{        .invocation = testInvocation(.ask),        .outcome = .completed,    });    view.runPostTurnEnd(.{        .invocation = testInvocation(.interactive),        .outcome = .completed,    });    view.runAttentionRequired(.{        .invocation = testInvocation(.acp),        .kind = .permission,    });    view.runAttentionRequired(.{        .invocation = testInvocation(.interactive),        .kind = .permission,    });    view.runAttentionRequired(.{        .invocation = testInvocation(.interactive),        .kind = .question,    });    view.runAttentionRequired(.{        .invocation = testInvocation(.interactive),        .kind = .route_recovery,    });     try std.testing.expectEqual(@as(usize, 6), app.herdr.report_count);    try expectReport(app.herdr.reports[0], .idle, null);    try expectReport(app.herdr.reports[1], .working, null);    try expectReport(app.herdr.reports[2], .idle, null);    try expectReport(app.herdr.reports[3], .blocked, "permission");    try expectReport(app.herdr.reports[4], .blocked, "question");    try expectReport(app.herdr.reports[5], .blocked, "recovery");} test "disabled built-in Herdr provider registers no lifecycle hooks" {    const TestApp = struct {        alloc: std.mem.Allocator,        lifecycle_runtime: hooks.Runtime,        herdr: RecordingClient = .{ .enable_on_init = false },    };     var app = TestApp{        .alloc = std.testing.allocator,        .lifecycle_runtime = hooks.Runtime.init(std.testing.allocator),    };    defer app.lifecycle_runtime.deinit();     try Runtime(TestApp).configure(&app, "session-42");    const view = app.lifecycle_runtime.freeze();    try std.testing.expect(app.herdr.initialized);    try std.testing.expect(!app.herdr.announced);    try std.testing.expect(app.herdr.session_id == null);    try std.testing.expectEqual(@as(usize, 0), app.herdr.report_count);    try std.testing.expect(!view.hasPostTurnEnd());    try std.testing.expect(!view.hasAttentionRequired());} fn testInvocation(kind: hooks.ScopeKind) hooks.Invocation {    return .{        .scope = .{            .kind = kind,            .workspace_root = "/tmp/workspace",            .session_id = "session",        },        .turn_id = 42,    };} fn expectReport(actual: RecordingClient.Report, state: herdr.State, status: ?[]const u8) !void {    try std.testing.expectEqual(state, actual.state);    if (status) |expected| {        try std.testing.expectEqualStrings(expected, actual.status orelse return error.TestExpectedEqual);    } else {        try std.testing.expect(actual.status == null);    }}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t