dev → main: rule-arm review, moment usage by rule, no-subject auto-inject, copy check, S3 test wording #215
@@ -44,6 +44,14 @@ export interface RemovedDefault {
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface MomentRuleUsage {
|
||||
rule_id: number;
|
||||
/** Null for a rule deleted since it was delivered. */
|
||||
title: string | null;
|
||||
delivered: number;
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
export interface MomentUsage {
|
||||
/** How many times a mounted rule arrived at this moment. */
|
||||
delivered: number;
|
||||
@@ -53,6 +61,8 @@ export interface MomentUsage {
|
||||
* the door, not the line that prompted it. */
|
||||
opened: number;
|
||||
last_delivered_at: string | null;
|
||||
/** Each rule delivered here, most delivered first. */
|
||||
by_rule: MomentRuleUsage[];
|
||||
}
|
||||
|
||||
export interface MomentsPayload {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import type { MomentRuleUsage } from "@/api/moments";
|
||||
|
||||
/**
|
||||
* Which rules a moment delivered, and whether an agent went on to read each.
|
||||
* The moment's own counts cannot say this once two rules are mounted there.
|
||||
* Closed by default: the counts in the row above are what most visits want.
|
||||
*/
|
||||
defineProps<{ rules: MomentRuleUsage[] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<details v-if="rules.length" class="moment-rules">
|
||||
<summary>Rules delivered here</summary>
|
||||
<ul class="delivered-list">
|
||||
<li v-for="r in rules" :key="r.rule_id" class="delivered-row">
|
||||
<span class="delivered-title">{{ r.title ?? `Rule ${r.rule_id} (deleted)` }}</span>
|
||||
<span class="delivered-count">delivered {{ r.delivered }}</span>
|
||||
<span :class="r.opened ? 'delivered-opened' : 'delivered-unopened'">
|
||||
{{ r.opened ? "opened" : "not opened" }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.moment-rules { margin-top: var(--fs-space-2); font-size: var(--fs-size-tiny); }
|
||||
.moment-rules summary { cursor: pointer; color: var(--fs-text-tertiary); }
|
||||
.delivered-list { list-style: none; margin: var(--fs-space-1) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--fs-space-1); }
|
||||
.delivered-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--fs-space-2); font-variant-numeric: tabular-nums; }
|
||||
.delivered-title { color: var(--fs-text-primary); font-size: var(--fs-size-body-sm); }
|
||||
.delivered-count { color: var(--fs-text-secondary); }
|
||||
.delivered-opened { color: var(--fs-text-secondary); }
|
||||
/* Not read is a fact about this window, not a fault: quiet, like "nothing mounted". */
|
||||
.delivered-unopened { color: var(--fs-text-tertiary); font-style: italic; }
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useMomentsStore } from "@/stores/moments";
|
||||
import MomentProposals from "@/components/MomentProposals.vue";
|
||||
import MomentRuleUsage from "@/components/MomentRuleUsage.vue";
|
||||
import type { MomentAction, MomentUsage } from "@/api/moments";
|
||||
import { fmtDate } from "@/utils/dateFormat";
|
||||
|
||||
@@ -167,6 +168,7 @@ onMounted(() => store.load(true));
|
||||
>×</button>
|
||||
</span>
|
||||
</div>
|
||||
<MomentRuleUsage v-if="usage(m.name)" :rules="usage(m.name)!.by_rule" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -186,6 +188,7 @@ onMounted(() => store.load(true));
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<MomentRuleUsage v-if="usage(name)" :rules="usage(name)!.by_rule" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
@@ -238,9 +238,10 @@ async def test_the_delivery_lookup_answers_by_home(world):
|
||||
|
||||
|
||||
async def test_moment_usage_counts_deliveries_and_the_agent_opens_that_followed(world):
|
||||
"""The per-moment readout, on real SQL: an EXISTS inside a
|
||||
COUNT(CASE …) is a shape no mock can vouch for, and the block's own guard
|
||||
would turn a database refusal into a quiet `moment_usage_failed`."""
|
||||
"""The per-moment readout, on real SQL: a correlated EXISTS selected as a
|
||||
column beside an outer join is a shape no mock can vouch for, and the
|
||||
block's own guard would turn a database refusal into a quiet
|
||||
`moment_usage_failed`."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import delete
|
||||
@@ -289,3 +290,13 @@ async def test_moment_usage_counts_deliveries_and_the_agent_opens_that_followed(
|
||||
report = block["by_moment"]["reply.report"]
|
||||
assert (report["delivered"], report["rules"], report["opened"]) == (1, 1, 0)
|
||||
assert report["last_delivered_at"]
|
||||
|
||||
# Two rules share work.finish: which one arrived, and which was opened.
|
||||
assert finish["by_rule"] == [
|
||||
{"rule_id": rule.id, "title": rule.title, "delivered": 2, "opened": True},
|
||||
{"rule_id": other.id, "title": "Close the task with the push",
|
||||
"delivered": 1, "opened": False},
|
||||
]
|
||||
assert report["by_rule"] == [
|
||||
{"rule_id": rule.id, "title": rule.title, "delivered": 1, "opened": False},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user