Desk: pin vectors to CultLib de8a062a (answerTooLarge) #45
5 changed files with 260 additions and 5 deletions
|
|
@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
|
|||
namespace Bifrost.Web.Features.Desk;
|
||||
|
||||
// The fork-desk documents Bifrost reads or writes, spelled as CultLib's contracts/cultnet/governance/fork-card.md
|
||||
// (commit f6c9bf5f) spells them: snake_case keys, every key written (nil where absent), declared in the
|
||||
// (commit de8a062a) spells them: snake_case keys, every key written (nil where absent), declared in the
|
||||
// contract's key order. Bifrost authors only DeskRefusal and DeskUnavailable. AnswerRequest is the client's body;
|
||||
// ForkCard, DeskAnswerDocument and AnswerResult are Huginn's documents, typed here so they pass through unchanged.
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,54 @@ public sealed class DeskAnswerTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_stale_answer_over_an_oversize_fork_follows_the_vector_fresh_and_replayed()
|
||||
{
|
||||
var rows = DeskVectors.Rows("transport", "answerTooLarge").ToList();
|
||||
Assert.Single(rows);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var (factory, token) = await ArrangeAsync();
|
||||
using (factory)
|
||||
{
|
||||
var request = row["request"]!;
|
||||
var forkId = (string)row["fork_id"]!;
|
||||
Assert.Equal(forkId, (string)request["fork_id"]!);
|
||||
var stale = new AnswerResult(
|
||||
DeskSchemas.AnswerResult,
|
||||
new DeskAnswerDocument(
|
||||
DeskSchemas.DeskAnswer, "n1", forkId, (string)row["outcome"]!, null, null, (string)row["code"]!,
|
||||
(string)request["options_hash"]!, (string?)request["answer"]!["choice"], (string?)request["answer"]!["note"],
|
||||
(string)request["answered_at"]!, "2026-11-12T19:02:05Z", false, DeskWebApplicationFactory.Alice, "Operator",
|
||||
(string)request["request_id"]!),
|
||||
null,
|
||||
null);
|
||||
factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(stale);
|
||||
factory.Door.ReadScript = (_, id) => new DeskReadOutcome.ForkTooLarge(id);
|
||||
var body = DeskVectors.Bytes(request);
|
||||
var target = DeskReplay.Target("answer", "gamecult-chat", null);
|
||||
|
||||
var fresh = await factory.SendAsync("POST", target, body, token);
|
||||
var replay = await factory.SendAsync("POST", target, body, token);
|
||||
|
||||
foreach (var reply in new[] { fresh, replay })
|
||||
{
|
||||
Assert.Equal((int)row["httpStatus"]!, reply.Status);
|
||||
var doc = JsonNode.Parse(reply.Body);
|
||||
Assert.True(DeskVectors.SameRefusal(doc, row["refusal"], forkId, null), (string)row["name"]!);
|
||||
Assert.Null(doc!["card"]);
|
||||
}
|
||||
|
||||
Assert.Equal(fresh.Body, replay.Body);
|
||||
// The door got the client's bytes untouched, stored nothing counted, and was asked for the card twice.
|
||||
Assert.All(factory.Door.Calls.Where(c => c.Kind == "answer"), c => Assert.Equal(body, c.Body));
|
||||
Assert.Equal(forkId, factory.Door.Calls.First(c => c.Kind == "read").ForkId);
|
||||
Assert.False((bool)row["counted"]!);
|
||||
Assert.NotEqual("Counted", stale.Answer.Outcome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Card_read_failure_is_unavailable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Bifrost.Web.Tests;
|
|||
public sealed class DeskVectorPinTests
|
||||
{
|
||||
[Fact]
|
||||
public void Vectors_are_cultlib_f6c9bf5f()
|
||||
public void Vectors_are_cultlib_de8a062a()
|
||||
{
|
||||
var sha = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(DeskVectors.Path))).ToLowerInvariant();
|
||||
|
||||
|
|
|
|||
|
|
@ -7702,6 +7702,213 @@
|
|||
"fork_id": "gamecult-chat:question:q3"
|
||||
}
|
||||
],
|
||||
"answerTooLarge": [
|
||||
{
|
||||
"name": "a_stale_answer_whose_card_reread_is_past_the_cap_is_fork_too_large_over_a_stored_stale_refusal",
|
||||
"entries": [
|
||||
{
|
||||
"schemaVersion": "gamecult.fork.entry.v0",
|
||||
"card": {
|
||||
"schemaVersion": "gamecult.fork.card.v0",
|
||||
"fork_id": "gamecult-chat:question:q3",
|
||||
"title": "Fork 3",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"leans": null,
|
||||
"flag": null,
|
||||
"raised_in": {
|
||||
"kind": "CutSpec",
|
||||
"id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4"
|
||||
},
|
||||
"deciders": [
|
||||
"e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f"
|
||||
],
|
||||
"status": "open",
|
||||
"ruling": null,
|
||||
"ruled_from": [],
|
||||
"order": 3,
|
||||
"options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2"
|
||||
},
|
||||
"record": {
|
||||
"schemaVersion": "gamecult.fork.record.v0",
|
||||
"fork_id": "gamecult-chat:question:q3",
|
||||
"options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2",
|
||||
"counted_answer_id": null,
|
||||
"answers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"schemaVersion": "gamecult.fork.entry.v0",
|
||||
"card": {
|
||||
"schemaVersion": "gamecult.fork.card.v0",
|
||||
"fork_id": "gamecult-chat:question:q2",
|
||||
"title": "Fork 2",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"leans": null,
|
||||
"flag": null,
|
||||
"raised_in": {
|
||||
"kind": "CutSpec",
|
||||
"id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4"
|
||||
},
|
||||
"deciders": [
|
||||
"e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f"
|
||||
],
|
||||
"status": "open",
|
||||
"ruling": null,
|
||||
"ruled_from": [],
|
||||
"order": 2,
|
||||
"options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79"
|
||||
},
|
||||
"record": {
|
||||
"schemaVersion": "gamecult.fork.record.v0",
|
||||
"fork_id": "gamecult-chat:question:q2",
|
||||
"options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79",
|
||||
"counted_answer_id": null,
|
||||
"answers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"schemaVersion": "gamecult.fork.entry.v0",
|
||||
"card": {
|
||||
"schemaVersion": "gamecult.fork.card.v0",
|
||||
"fork_id": "gamecult-chat:question:q1",
|
||||
"title": "Fork 1",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"leans": null,
|
||||
"flag": null,
|
||||
"raised_in": {
|
||||
"kind": "CutSpec",
|
||||
"id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4"
|
||||
},
|
||||
"deciders": [
|
||||
"e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f"
|
||||
],
|
||||
"status": "open",
|
||||
"ruling": null,
|
||||
"ruled_from": [],
|
||||
"order": 1,
|
||||
"options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a"
|
||||
},
|
||||
"record": {
|
||||
"schemaVersion": "gamecult.fork.record.v0",
|
||||
"fork_id": "gamecult-chat:question:q1",
|
||||
"options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a",
|
||||
"counted_answer_id": null,
|
||||
"answers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"schemaVersion": "gamecult.fork.entry.v0",
|
||||
"card": {
|
||||
"schemaVersion": "gamecult.fork.card.v0",
|
||||
"fork_id": "gamecult-chat:question:q0",
|
||||
"title": "Fork 0",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"leans": null,
|
||||
"flag": null,
|
||||
"raised_in": {
|
||||
"kind": "CutSpec",
|
||||
"id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4"
|
||||
},
|
||||
"deciders": [
|
||||
"e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f"
|
||||
],
|
||||
"status": "open",
|
||||
"ruling": null,
|
||||
"ruled_from": [],
|
||||
"order": 0,
|
||||
"options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d"
|
||||
},
|
||||
"record": {
|
||||
"schemaVersion": "gamecult.fork.record.v0",
|
||||
"fork_id": "gamecult-chat:question:q0",
|
||||
"options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d",
|
||||
"counted_answer_id": null,
|
||||
"answers": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"schemaVersion": "gamecult.fork.answer_request.v0",
|
||||
"fork_id": "gamecult-chat:question:q3",
|
||||
"options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc",
|
||||
"answer": {
|
||||
"choice": "yes",
|
||||
"note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act."
|
||||
},
|
||||
"retracted": false,
|
||||
"retracts": null,
|
||||
"answered_at": "2026-11-12T19:02:00Z",
|
||||
"request_id": "0000a001-0000-4000-8000-00000000a001"
|
||||
},
|
||||
"refusal": {
|
||||
"schemaVersion": "gamecult.fork.refusal.v0",
|
||||
"code": "fork_too_large",
|
||||
"error": "fork_too_large: fork gamecult-chat:question:q3 alone is past the response budget"
|
||||
},
|
||||
"refusalHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AE666F726B5F746F6F5F6C61726765A56572726F72D950666F726B5F746F6F5F6C617267653A20666F726B2067616D6563756C742D636861743A7175657374696F6E3A713320616C6F6E6520697320706173742074686520726573706F6E736520627564676574",
|
||||
"httpStatus": 422,
|
||||
"outcome": "Refused",
|
||||
"code": "stale_fork",
|
||||
"counted": false,
|
||||
"fork_id": "gamecult-chat:question:q3"
|
||||
}
|
||||
],
|
||||
"doorFaults": [
|
||||
{
|
||||
"name": "read_door_answers_request_id_conflict_the_client_sees_unavailable",
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ using System.Text.Json.Nodes;
|
|||
|
||||
namespace Bifrost.Web.Tests.Support;
|
||||
|
||||
// CultLib's fork-card vectors, vendored at commit f6c9bf5f975177d2033578e9b51b4d21930b728a.
|
||||
// CultLib's fork-card vectors, vendored at commit de8a062ab9d8c510c8b36e634fae8f108a570aff.
|
||||
public static class DeskVectors
|
||||
{
|
||||
public const string SourceCommit = "f6c9bf5f975177d2033578e9b51b4d21930b728a";
|
||||
public const string SourceCommit = "de8a062ab9d8c510c8b36e634fae8f108a570aff";
|
||||
|
||||
public const string FileSha256 = "cc15fc9e36e9c1f7d77b42279dc0f9a72c392adb251cc6ae3a59b79b7a004619";
|
||||
public const string FileSha256 = "a7129eee00cb38c602481b0de7de0182f83122685b69ff441e3123adc3e0f774";
|
||||
|
||||
public static string Path => System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", "fork-card-vectors.json");
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue