Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions graphcode-windows/src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10448,6 +10448,30 @@ test "graph publication adopts the daemon canonical path for the shell's own fol
}
}

test "open workspace applies production stopped state and removes Stop control" {
const F = GraphPublicationTest;
var app = try F.init(.v2);
defer F.deinit(&app);
try F.seed(&app);
app.surface = .workspace;

app.onFrameWithEffects(
\\{"version":2,"kind":"event","sequence":4,"event":{"graphChanged":{"_0":{"project":{"path":"B","name":"Beta"},"nodes":[{"id":"other","title":"Other","state":{"idle":{}}},{"id":"loop","title":"Beta loop","state":{"stopped":{}}}],"edges":[]}}}}
, F.rebind, F.refresh, F.publish);

const graph = workspaceGraph(&app.model) orelse return error.WorkspaceGraphMissing;
const selected_index = app.model.selectedIndex() orelse return error.WorkspaceSelectionMissing;
try std.testing.expectEqualStrings("loop", graph.nodes.items[selected_index].id);
try std.testing.expectEqualStrings("stopped", graph.nodes.items[selected_index].state);
const loop_bar = TerminalWorkspace.loopBarLayout(
Tokens.sidebar_width,
1200 - Tokens.loop_detail_width,
isResolvedLoopState(graph.nodes.items[selected_index].state),
);
try std.testing.expect(loop_bar.stop == null);
try std.testing.expectEqual(@as(usize, 1), F.publications);
}

/// Swift's `UUID.uuidString` echoes request IDs in uppercase.
fn upperRequestID(request: [36]u8) [36]u8 {
var upper: [36]u8 = undefined;
Expand Down
76 changes: 75 additions & 1 deletion graphcode-windows/src/GraphModel.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,41 @@ const JsonFields = struct {
}
};

fn isLoopState(value: []const u8) bool {
return for ([_][]const u8{
"idle",
"running",
"awaitingInput",
"blocked",
"succeeded",
"failed",
"stalled",
"waiting",
"stopped",
}) |state| {
if (std.mem.eql(u8, value, state)) break true;
} else false;
}

fn decodeLoopState(allocator: std.mem.Allocator, value: JsonValue) ![]u8 {
if (value.isNull()) return allocator.dupe(u8, "idle");
if (value.container('"') != null) {
const state = try value.duplicateString(allocator, "");
errdefer allocator.free(state);
if (!isLoopState(state)) return error.UnsupportedLoopState;
return state;
}
const object = value.container('{') orelse return error.MalformedLoopState;
var fields = try JsonFields.init(allocator, object);
defer fields.deinit();
if (fields.fields.items.len == 0) return allocator.dupe(u8, "idle");
if (fields.fields.items.len != 1) return error.MalformedLoopState;
const field = fields.fields.items[0];
if (!isLoopState(field.key) or field.value.container('{') == null)
return error.UnsupportedLoopState;
return allocator.dupe(u8, field.key);
}

fn validateGraphStructure(allocator: std.mem.Allocator, bytes: []const u8) !void {
return validateGraphStructureMeasured(allocator, bytes, null);
}
Expand Down Expand Up @@ -1835,7 +1870,7 @@ fn decodeNodes(
node.id = try fields.get("id").duplicateString(allocator, "");
node.title = try fields.get("title").duplicateString(allocator, "Untitled");
node.loop_type = try fields.get("loopType").duplicateString(allocator, "turnBased");
node.state = try fields.get("state").duplicateString(allocator, "idle");
node.state = try decodeLoopState(allocator, fields.get("state"));
node.activity = try fields.get("activity").duplicateString(allocator, "");
const presence = fields.get("presence");
if (presence.container('{')) |reading| {
Expand Down Expand Up @@ -3626,6 +3661,45 @@ test "production daemon graphChanged associated-value frame decodes project and
try std.testing.expectEqualStrings("C:/GraphCode-Fixtures/Core", model.graph.?.project.path);
}

test "production LoopState objects decode exhaustively and malformed states preserve the prior graph" {
const allocator = std.testing.allocator;
var nodes = std.array_list.Managed(Node).init(allocator);
defer {
for (nodes.items) |node| freeNode(allocator, node);
nodes.deinit();
}
try decodeNodes(
allocator,
\\[{"id":"idle","state":{"idle":{}}},{"id":"running","state":{"running":{}}},{"id":"awaiting","state":{"awaitingInput":{}}},{"id":"blocked","state":{"blocked":{}}},{"id":"succeeded","state":{"succeeded":{}}},{"id":"failed","state":{"failed":{}}},{"id":"stalled","state":{"stalled":{}}},{"id":"waiting","state":{"waiting":{}}},{"id":"stopped","state":{"stopped":{}}}]
,
&nodes,
);
for (nodes.items, [_][]const u8{
"idle",
"running",
"awaitingInput",
"blocked",
"succeeded",
"failed",
"stalled",
"waiting",
"stopped",
}) |node, expected| try std.testing.expectEqualStrings(expected, node.state);

var model = Model.init(allocator);
defer model.deinit();
_ = try model.updateFromFrame(
\\{"graphChanged":{"project":{"path":"C:\\work\\graph","name":"Graph"},"nodes":[{"id":"loop","title":"Loop","state":{"running":{}}}],"edges":[]}}
);
try std.testing.expectError(
error.UnsupportedLoopState,
model.updateFromFrame(
\\{"graphChanged":{"project":{"path":"C:\\work\\graph","name":"Graph"},"nodes":[{"id":"loop","title":"Loop","state":{"paused":{}}}],"edges":[]}}
),
);
try std.testing.expectEqualStrings("running", model.currentGraph().?.nodes.items[0].state);
}

test "field scope keys and delimiters inside scalar text cannot select graph fields" {
const frame =
\\{"metadata":{"graphChanged":{"project":{"path":"wrong"},"nodes":[],"edges":[]}},"event":{"graphChanged":{"note":"\"nodes\": [{\"id\":\"fake\"}], \"edges\": [{}] } \\ \u2603","nodes":[{"note":"\"id\":\"fake\" } [","id" : "real\u2603","title" : "Brace } quote \" slash \\ \uD83D\uDE80"}],"edges":[],"project":{"note":{"path":"wrong"},"path" : "right","name":"Root"}}}}
Expand Down
Loading