fix(signals): source order follows agent selection, not chronological timestamps
Remove the OutOfOrderSource constraint from composeSignal. The agent's selection order is authoritative for source ordinals; timestamps remain as per-source provenance metadata. This resolves the contract conflict where reversed message selections (valid agent compositions) were rejected by the primitive. Each source still retains its original createdAt, so chronological provenance is preserved exactly. The sourceKey includes ordered message IDs, so different orderings produce distinct signals with deterministic idempotency. Coverage added: - Primitive: non-chronological order accepted, timestamps preserved - Backend: reversed selection preserves ordinals and exact raw text
This commit is contained in:
@@ -412,6 +412,69 @@ describe("signals", () => {
|
||||
expect(list).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("reversed selection preserves agent ordinals and original timestamps", async () => {
|
||||
const t = newTest();
|
||||
const orgId = await ensureOrg(t, identityA);
|
||||
const m1 = await seedMessage(t, identityA, orgId, "req-rev-1", "first");
|
||||
const m2 = await seedMessage(t, identityA, orgId, "req-rev-2", "second");
|
||||
|
||||
// Chronological selection: m1 then m2.
|
||||
const chrono = await t
|
||||
.withIdentity(identityA)
|
||||
.mutation(api.signals.createFromMessages, {
|
||||
conversationId: orgId,
|
||||
messageIds: [m1, m2],
|
||||
organizationId: orgId,
|
||||
problemStatement,
|
||||
processedBy,
|
||||
});
|
||||
|
||||
// Reversed selection: m2 then m1 (non-chronological agent order).
|
||||
const reversed = await t
|
||||
.withIdentity(identityA)
|
||||
.mutation(api.signals.createFromMessages, {
|
||||
conversationId: orgId,
|
||||
messageIds: [m2, m1],
|
||||
organizationId: orgId,
|
||||
problemStatement,
|
||||
processedBy,
|
||||
});
|
||||
|
||||
expect(reversed.signalId).not.toBe(chrono.signalId);
|
||||
|
||||
const chronoSignal = await t
|
||||
.withIdentity(identityA)
|
||||
.query(api.signals.get, { signalId: chrono.signalId });
|
||||
const reversedSignal = await t
|
||||
.withIdentity(identityA)
|
||||
.query(api.signals.get, { signalId: reversed.signalId });
|
||||
|
||||
// Chronological: ordinals follow selection.
|
||||
expect(chronoSignal?.sources[0]?.messageId).toBe(m1);
|
||||
expect(chronoSignal?.sources[0]?.ordinal).toBe(0);
|
||||
expect(chronoSignal?.sources[1]?.messageId).toBe(m2);
|
||||
expect(chronoSignal?.sources[1]?.ordinal).toBe(1);
|
||||
|
||||
// Reversed: ordinals follow the agent's reversed selection, and each
|
||||
// source retains its original creation timestamp (unchanged by reordering).
|
||||
expect(reversedSignal?.sources[0]?.messageId).toBe(m2);
|
||||
expect(reversedSignal?.sources[0]?.ordinal).toBe(0);
|
||||
expect(reversedSignal?.sources[1]?.messageId).toBe(m1);
|
||||
expect(reversedSignal?.sources[1]?.ordinal).toBe(1);
|
||||
|
||||
// Exact raw text is preserved in both orderings.
|
||||
expect(
|
||||
chronoSignal?.sources.map(
|
||||
(s: { rawTextSnapshot: string }) => s.rawTextSnapshot
|
||||
)
|
||||
).toEqual(["first", "second"]);
|
||||
expect(
|
||||
reversedSignal?.sources.map(
|
||||
(s: { rawTextSnapshot: string }) => s.rawTextSnapshot
|
||||
)
|
||||
).toEqual(["second", "first"]);
|
||||
});
|
||||
|
||||
test("creates a project-scoped signal when the project owner is an org member", async () => {
|
||||
const t = newTest();
|
||||
const orgId = await ensureOrg(t, identityA);
|
||||
|
||||
Reference in New Issue
Block a user