@agent-native/dispatch 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/actions/apply-dream-proposal.d.ts +3 -0
- package/dist/actions/apply-dream-proposal.d.ts.map +1 -0
- package/dist/actions/apply-dream-proposal.js +11 -0
- package/dist/actions/apply-dream-proposal.js.map +1 -0
- package/dist/actions/create-dream-report.d.ts +3 -0
- package/dist/actions/create-dream-report.d.ts.map +1 -0
- package/dist/actions/create-dream-report.js +67 -0
- package/dist/actions/create-dream-report.js.map +1 -0
- package/dist/actions/create-workspace-resource.js +3 -3
- package/dist/actions/create-workspace-resource.js.map +1 -1
- package/dist/actions/delete-workspace-resource.js +1 -1
- package/dist/actions/delete-workspace-resource.js.map +1 -1
- package/dist/actions/ensure-dream-job.d.ts +3 -0
- package/dist/actions/ensure-dream-job.d.ts.map +1 -0
- package/dist/actions/ensure-dream-job.js +73 -0
- package/dist/actions/ensure-dream-job.js.map +1 -0
- package/dist/actions/get-dream-settings.d.ts +3 -0
- package/dist/actions/get-dream-settings.d.ts.map +1 -0
- package/dist/actions/get-dream-settings.js +11 -0
- package/dist/actions/get-dream-settings.js.map +1 -0
- package/dist/actions/get-dream.d.ts +3 -0
- package/dist/actions/get-dream.d.ts.map +1 -0
- package/dist/actions/get-dream.js +13 -0
- package/dist/actions/get-dream.js.map +1 -0
- package/dist/actions/get-workspace-resource-effective-context.d.ts +3 -0
- package/dist/actions/get-workspace-resource-effective-context.d.ts.map +1 -0
- package/dist/actions/get-workspace-resource-effective-context.js +27 -0
- package/dist/actions/get-workspace-resource-effective-context.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +30 -4
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-dream-candidates.d.ts +3 -0
- package/dist/actions/list-dream-candidates.d.ts.map +1 -0
- package/dist/actions/list-dream-candidates.js +68 -0
- package/dist/actions/list-dream-candidates.js.map +1 -0
- package/dist/actions/list-dreams.d.ts +3 -0
- package/dist/actions/list-dreams.d.ts.map +1 -0
- package/dist/actions/list-dreams.js +17 -0
- package/dist/actions/list-dreams.js.map +1 -0
- package/dist/actions/list-workspace-resources-for-app.d.ts +3 -0
- package/dist/actions/list-workspace-resources-for-app.d.ts.map +1 -0
- package/dist/actions/list-workspace-resources-for-app.js +12 -0
- package/dist/actions/list-workspace-resources-for-app.js.map +1 -0
- package/dist/actions/list-workspace-resources.js +1 -1
- package/dist/actions/list-workspace-resources.js.map +1 -1
- package/dist/actions/navigate.d.ts +1 -0
- package/dist/actions/navigate.d.ts.map +1 -1
- package/dist/actions/navigate.js +2 -1
- package/dist/actions/navigate.js.map +1 -1
- package/dist/actions/preview-dream-proposal.d.ts +3 -0
- package/dist/actions/preview-dream-proposal.d.ts.map +1 -0
- package/dist/actions/preview-dream-proposal.js +13 -0
- package/dist/actions/preview-dream-proposal.js.map +1 -0
- package/dist/actions/preview-workspace-resource-change.d.ts +3 -0
- package/dist/actions/preview-workspace-resource-change.d.ts.map +1 -0
- package/dist/actions/preview-workspace-resource-change.js +24 -0
- package/dist/actions/preview-workspace-resource-change.js.map +1 -0
- package/dist/actions/reject-dream-proposal.d.ts +3 -0
- package/dist/actions/reject-dream-proposal.d.ts.map +1 -0
- package/dist/actions/reject-dream-proposal.js +12 -0
- package/dist/actions/reject-dream-proposal.js.map +1 -0
- package/dist/actions/restore-starter-workspace-resources.d.ts +3 -0
- package/dist/actions/restore-starter-workspace-resources.d.ts.map +1 -0
- package/dist/actions/restore-starter-workspace-resources.js +14 -0
- package/dist/actions/restore-starter-workspace-resources.js.map +1 -0
- package/dist/actions/send-code-agent-remote-command.d.ts +3 -0
- package/dist/actions/send-code-agent-remote-command.d.ts.map +1 -0
- package/dist/actions/send-code-agent-remote-command.js +53 -0
- package/dist/actions/send-code-agent-remote-command.js.map +1 -0
- package/dist/actions/set-dream-settings.d.ts +3 -0
- package/dist/actions/set-dream-settings.d.ts.map +1 -0
- package/dist/actions/set-dream-settings.js +41 -0
- package/dist/actions/set-dream-settings.js.map +1 -0
- package/dist/actions/update-workspace-resource.js +1 -1
- package/dist/actions/update-workspace-resource.js.map +1 -1
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +73 -2
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/approval-value-block.d.ts +7 -0
- package/dist/components/approval-value-block.d.ts.map +1 -0
- package/dist/components/approval-value-block.js +22 -0
- package/dist/components/approval-value-block.js.map +1 -0
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +3 -2
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -1
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/ui/chart.d.ts +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +25 -4
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-resource-effective-stack.d.ts +11 -0
- package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -0
- package/dist/components/workspace-resource-effective-stack.js +59 -0
- package/dist/components/workspace-resource-effective-stack.js.map +1 -0
- package/dist/components/workspace-resource-impact-preview.d.ts +9 -0
- package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -0
- package/dist/components/workspace-resource-impact-preview.js +39 -0
- package/dist/components/workspace-resource-impact-preview.js.map +1 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +59 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/schema.d.ts +714 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +44 -2
- package/dist/db/schema.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +3 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +23 -3
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/utils.d.ts +2 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +5 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/approval.d.ts.map +1 -1
- package/dist/routes/pages/approval.js +4 -1
- package/dist/routes/pages/approval.js.map +1 -1
- package/dist/routes/pages/approvals.js +1 -1
- package/dist/routes/pages/approvals.js.map +1 -1
- package/dist/routes/pages/dream-settings.d.ts +34 -0
- package/dist/routes/pages/dream-settings.d.ts.map +1 -0
- package/dist/routes/pages/dream-settings.js +68 -0
- package/dist/routes/pages/dream-settings.js.map +1 -0
- package/dist/routes/pages/dreams.d.ts +5 -0
- package/dist/routes/pages/dreams.d.ts.map +1 -0
- package/dist/routes/pages/dreams.js +435 -0
- package/dist/routes/pages/dreams.js.map +1 -0
- package/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +187 -35
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +3 -2
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +9 -4
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/dist/server/lib/dispatch-remote-commands.d.ts +83 -0
- package/dist/server/lib/dispatch-remote-commands.d.ts.map +1 -0
- package/dist/server/lib/dispatch-remote-commands.js +256 -0
- package/dist/server/lib/dispatch-remote-commands.js.map +1 -0
- package/dist/server/lib/dispatch-store.d.ts +26 -0
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +17 -1
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/dreams-store.d.ts +398 -0
- package/dist/server/lib/dreams-store.d.ts.map +1 -0
- package/dist/server/lib/dreams-store.js +2330 -0
- package/dist/server/lib/dreams-store.js.map +1 -0
- package/dist/server/lib/thread-debug-store.d.ts +2 -2
- package/dist/server/lib/vault-store.d.ts +1 -1
- package/dist/server/lib/workspace-resources-store.d.ts +181 -17
- package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
- package/dist/server/lib/workspace-resources-store.js +737 -108
- package/dist/server/lib/workspace-resources-store.js.map +1 -1
- package/package.json +4 -2
- package/src/actions/apply-dream-proposal.ts +12 -0
- package/src/actions/create-dream-report.ts +76 -0
- package/src/actions/create-workspace-resource.ts +3 -3
- package/src/actions/delete-workspace-resource.ts +1 -1
- package/src/actions/ensure-dream-job.ts +76 -0
- package/src/actions/get-dream-settings.ts +12 -0
- package/src/actions/get-dream.ts +14 -0
- package/src/actions/get-workspace-resource-effective-context.ts +34 -0
- package/src/actions/index.spec.ts +26 -0
- package/src/actions/index.ts +31 -4
- package/src/actions/list-dream-candidates.ts +77 -0
- package/src/actions/list-dreams.ts +17 -0
- package/src/actions/list-workspace-resources-for-app.ts +13 -0
- package/src/actions/list-workspace-resources.ts +1 -1
- package/src/actions/navigate.ts +2 -1
- package/src/actions/preview-dream-proposal.ts +14 -0
- package/src/actions/preview-workspace-resource-change.ts +25 -0
- package/src/actions/reject-dream-proposal.ts +12 -0
- package/src/actions/restore-starter-workspace-resources.ts +17 -0
- package/src/actions/send-code-agent-remote-command.ts +59 -0
- package/src/actions/set-dream-settings.spec.ts +81 -0
- package/src/actions/set-dream-settings.ts +44 -0
- package/src/actions/update-workspace-resource.ts +1 -1
- package/src/actions/view-screen.ts +90 -2
- package/src/components/approval-value-block.spec.tsx +59 -0
- package/src/components/approval-value-block.tsx +33 -0
- package/src/components/create-app-popover.tsx +3 -2
- package/src/components/layout/Layout.tsx +8 -0
- package/src/components/workspace-app-card.tsx +166 -1
- package/src/components/workspace-resource-effective-stack.spec.tsx +125 -0
- package/src/components/workspace-resource-effective-stack.tsx +141 -0
- package/src/components/workspace-resource-impact-preview.spec.tsx +147 -0
- package/src/components/workspace-resource-impact-preview.tsx +116 -0
- package/src/db/migrations.ts +59 -0
- package/src/db/schema.ts +46 -2
- package/src/hooks/use-navigation-state.ts +24 -5
- package/src/lib/utils.ts +6 -1
- package/src/routes/index.ts +1 -0
- package/src/routes/pages/approval.tsx +14 -1
- package/src/routes/pages/approvals.tsx +1 -1
- package/src/routes/pages/dream-settings.spec.ts +130 -0
- package/src/routes/pages/dream-settings.ts +103 -0
- package/src/routes/pages/dreams.tsx +1828 -0
- package/src/routes/pages/workspace.tsx +577 -97
- package/src/server/lib/app-creation-store.ts +3 -2
- package/src/server/lib/dispatch-integrations.ts +10 -3
- package/src/server/lib/dispatch-remote-commands.spec.ts +167 -0
- package/src/server/lib/dispatch-remote-commands.ts +375 -0
- package/src/server/lib/dispatch-store.ts +37 -1
- package/src/server/lib/dreams-store.spec.ts +1492 -0
- package/src/server/lib/dreams-store.ts +3168 -0
- package/src/server/lib/workspace-resource-approval-lifecycle.spec.ts +236 -0
- package/src/server/lib/workspace-resources-store.spec.ts +1106 -0
- package/src/server/lib/workspace-resources-store.ts +1001 -134
- package/dist/actions/sync-workspace-resources-to-all.d.ts +0 -3
- package/dist/actions/sync-workspace-resources-to-all.d.ts.map +0 -1
- package/dist/actions/sync-workspace-resources-to-all.js +0 -9
- package/dist/actions/sync-workspace-resources-to-all.js.map +0 -1
- package/dist/actions/sync-workspace-resources-to-app.d.ts +0 -3
- package/dist/actions/sync-workspace-resources-to-app.d.ts.map +0 -1
- package/dist/actions/sync-workspace-resources-to-app.js +0 -11
- package/dist/actions/sync-workspace-resources-to-app.js.map +0 -1
- package/src/actions/sync-workspace-resources-to-all.ts +0 -10
- package/src/actions/sync-workspace-resources-to-app.ts +0 -12
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
getDb: vi.fn(),
|
|
5
|
+
currentOwnerEmail: vi.fn(() => "owner@example.test"),
|
|
6
|
+
currentOrgId: vi.fn(() => null),
|
|
7
|
+
getApprovalPolicy: vi.fn(),
|
|
8
|
+
createApprovalRequest: vi.fn(),
|
|
9
|
+
recordAudit: vi.fn(),
|
|
10
|
+
searchAgentThreads: vi.fn(),
|
|
11
|
+
getAgentThreadDebug: vi.fn(),
|
|
12
|
+
listThreadDebugSources: vi.fn(),
|
|
13
|
+
resourceGetByPath: vi.fn(),
|
|
14
|
+
resourceList: vi.fn(),
|
|
15
|
+
resourcePut: vi.fn(),
|
|
16
|
+
getOrgSetting: vi.fn(),
|
|
17
|
+
getUserSetting: vi.fn(),
|
|
18
|
+
putOrgSetting: vi.fn(),
|
|
19
|
+
putUserSetting: vi.fn(),
|
|
20
|
+
createWorkspaceResource: vi.fn(),
|
|
21
|
+
listWorkspaceResources: vi.fn(),
|
|
22
|
+
updateWorkspaceResource: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("../../db/index.js", async () => {
|
|
26
|
+
const schemaModule =
|
|
27
|
+
await vi.importActual<typeof import("../../db/schema.js")>(
|
|
28
|
+
"../../db/schema.js",
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
...schemaModule,
|
|
32
|
+
schema: schemaModule,
|
|
33
|
+
getDb: mocks.getDb,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
vi.mock("./dispatch-store.js", () => ({
|
|
38
|
+
currentOwnerEmail: mocks.currentOwnerEmail,
|
|
39
|
+
currentOrgId: mocks.currentOrgId,
|
|
40
|
+
getApprovalPolicy: mocks.getApprovalPolicy,
|
|
41
|
+
createApprovalRequest: mocks.createApprovalRequest,
|
|
42
|
+
recordAudit: mocks.recordAudit,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
vi.mock("./thread-debug-store.js", () => ({
|
|
46
|
+
searchAgentThreads: mocks.searchAgentThreads,
|
|
47
|
+
getAgentThreadDebug: mocks.getAgentThreadDebug,
|
|
48
|
+
listThreadDebugSources: mocks.listThreadDebugSources,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock("@agent-native/core/resources/store", () => ({
|
|
52
|
+
SHARED_OWNER: "__shared__",
|
|
53
|
+
resourceGetByPath: mocks.resourceGetByPath,
|
|
54
|
+
resourceList: mocks.resourceList,
|
|
55
|
+
resourcePut: mocks.resourcePut,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
vi.mock("@agent-native/core/settings", () => ({
|
|
59
|
+
getOrgSetting: mocks.getOrgSetting,
|
|
60
|
+
getUserSetting: mocks.getUserSetting,
|
|
61
|
+
putOrgSetting: mocks.putOrgSetting,
|
|
62
|
+
putUserSetting: mocks.putUserSetting,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
vi.mock("./workspace-resources-store.js", () => ({
|
|
66
|
+
createWorkspaceResource: mocks.createWorkspaceResource,
|
|
67
|
+
listWorkspaceResources: mocks.listWorkspaceResources,
|
|
68
|
+
updateWorkspaceResource: mocks.updateWorkspaceResource,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
import { schema } from "../../db/index.js";
|
|
72
|
+
import {
|
|
73
|
+
applyApprovedDreamProposal,
|
|
74
|
+
applyDreamProposal,
|
|
75
|
+
buildProposalInputs,
|
|
76
|
+
ensureDreamJob,
|
|
77
|
+
getDreamSettings,
|
|
78
|
+
listDreamCandidates,
|
|
79
|
+
previewDreamProposal,
|
|
80
|
+
setDreamSettings,
|
|
81
|
+
type DreamCandidate,
|
|
82
|
+
type DreamEvidence,
|
|
83
|
+
} from "./dreams-store.js";
|
|
84
|
+
|
|
85
|
+
function resource(path: string, content: string, owner = "owner@example.test") {
|
|
86
|
+
return {
|
|
87
|
+
id: `res-${path}`,
|
|
88
|
+
owner,
|
|
89
|
+
path,
|
|
90
|
+
content,
|
|
91
|
+
mimeType: "text/markdown",
|
|
92
|
+
size: Buffer.byteLength(content, "utf8"),
|
|
93
|
+
createdAt: 1,
|
|
94
|
+
updatedAt: 2,
|
|
95
|
+
createdBy: "agent",
|
|
96
|
+
visibility: "workspace",
|
|
97
|
+
threadId: null,
|
|
98
|
+
runId: null,
|
|
99
|
+
expiresAt: null,
|
|
100
|
+
metadata: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resourceWithMime(
|
|
105
|
+
path: string,
|
|
106
|
+
content: string,
|
|
107
|
+
owner = "owner@example.test",
|
|
108
|
+
mimeType = "text/markdown",
|
|
109
|
+
) {
|
|
110
|
+
return {
|
|
111
|
+
...resource(path, content, owner),
|
|
112
|
+
mimeType,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createDbMock(proposal?: Record<string, unknown>) {
|
|
117
|
+
let currentProposal = proposal;
|
|
118
|
+
return {
|
|
119
|
+
insert: vi.fn(() => ({ values: vi.fn(async () => undefined) })),
|
|
120
|
+
update: vi.fn((table) => ({
|
|
121
|
+
set: vi.fn((values) => ({
|
|
122
|
+
where: vi.fn(async () => {
|
|
123
|
+
if (table === schema.dispatchDreamProposals && currentProposal) {
|
|
124
|
+
currentProposal = { ...currentProposal, ...values };
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
})),
|
|
128
|
+
})),
|
|
129
|
+
select: vi.fn(() => ({
|
|
130
|
+
from: vi.fn((table) => ({
|
|
131
|
+
where: vi.fn(() => ({
|
|
132
|
+
limit: vi.fn(async () => {
|
|
133
|
+
if (table === schema.dispatchDreamProposals && currentProposal) {
|
|
134
|
+
return [currentProposal];
|
|
135
|
+
}
|
|
136
|
+
return [];
|
|
137
|
+
}),
|
|
138
|
+
orderBy: vi.fn(async () => {
|
|
139
|
+
if (table === schema.dispatchDreamProposals && currentProposal) {
|
|
140
|
+
return [currentProposal];
|
|
141
|
+
}
|
|
142
|
+
return [];
|
|
143
|
+
}),
|
|
144
|
+
})),
|
|
145
|
+
orderBy: vi.fn(() => ({
|
|
146
|
+
limit: vi.fn(async () => []),
|
|
147
|
+
})),
|
|
148
|
+
})),
|
|
149
|
+
})),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function explicitEvidence(
|
|
154
|
+
overrides: Partial<DreamEvidence> = {},
|
|
155
|
+
): DreamEvidence {
|
|
156
|
+
return {
|
|
157
|
+
kind: "explicit-correction",
|
|
158
|
+
label: "User corrected the agent",
|
|
159
|
+
snippet:
|
|
160
|
+
"Actually, remember to use shadcn DropdownMenu for action menus next time",
|
|
161
|
+
threadId: "thread-1",
|
|
162
|
+
threadTitle: "Memory correction",
|
|
163
|
+
messageIndex: 0,
|
|
164
|
+
createdAt: 1,
|
|
165
|
+
...overrides,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function candidateWithEvidence(
|
|
170
|
+
evidence: DreamEvidence[],
|
|
171
|
+
sourceId = "current",
|
|
172
|
+
): DreamCandidate {
|
|
173
|
+
return {
|
|
174
|
+
thread: {
|
|
175
|
+
id: evidence[0]?.threadId ?? "thread-1",
|
|
176
|
+
ownerEmail: "owner@example.test",
|
|
177
|
+
title: evidence[0]?.threadTitle ?? "Dream thread",
|
|
178
|
+
preview: "preview",
|
|
179
|
+
messageCount: 1,
|
|
180
|
+
createdAt: 1,
|
|
181
|
+
updatedAt: 2,
|
|
182
|
+
},
|
|
183
|
+
sourceId,
|
|
184
|
+
score: 50,
|
|
185
|
+
reasons: [
|
|
186
|
+
{
|
|
187
|
+
code: "explicit-correction",
|
|
188
|
+
label: "User corrections should be considered for memory",
|
|
189
|
+
score: 25,
|
|
190
|
+
evidenceCount: evidence.length,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
evidenceCounts: {},
|
|
194
|
+
evidence,
|
|
195
|
+
latestRunStatus: null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function pendingProposal(overrides: Record<string, unknown> = {}) {
|
|
200
|
+
return {
|
|
201
|
+
id: "proposal-1",
|
|
202
|
+
dreamId: "dream-1",
|
|
203
|
+
ownerEmail: "owner@example.test",
|
|
204
|
+
orgId: null,
|
|
205
|
+
targetType: "personal-memory",
|
|
206
|
+
targetPath: "memory/custom.md",
|
|
207
|
+
title: "Save explicit user corrections",
|
|
208
|
+
summary: "Remember a user-grounded Dispatch correction.",
|
|
209
|
+
rationale: "Explicit user corrections are high-signal evidence.",
|
|
210
|
+
content: "# Dispatch Dream Memory\n\nUse shadcn menus.",
|
|
211
|
+
evidence: JSON.stringify([explicitEvidence()]),
|
|
212
|
+
confidence: 80,
|
|
213
|
+
risk: "low",
|
|
214
|
+
status: "pending",
|
|
215
|
+
appliedBy: null,
|
|
216
|
+
appliedAt: null,
|
|
217
|
+
rejectedBy: null,
|
|
218
|
+
rejectedAt: null,
|
|
219
|
+
createdAt: 1,
|
|
220
|
+
updatedAt: 1,
|
|
221
|
+
...overrides,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
beforeEach(() => {
|
|
226
|
+
vi.clearAllMocks();
|
|
227
|
+
mocks.currentOwnerEmail.mockReturnValue("owner@example.test");
|
|
228
|
+
mocks.currentOrgId.mockReturnValue(null);
|
|
229
|
+
mocks.getApprovalPolicy.mockResolvedValue({
|
|
230
|
+
enabled: false,
|
|
231
|
+
approverEmails: [],
|
|
232
|
+
});
|
|
233
|
+
mocks.createApprovalRequest.mockResolvedValue({
|
|
234
|
+
id: "approval-1",
|
|
235
|
+
status: "pending",
|
|
236
|
+
});
|
|
237
|
+
mocks.listThreadDebugSources.mockResolvedValue({
|
|
238
|
+
access: {
|
|
239
|
+
viewerEmail: "owner@example.test",
|
|
240
|
+
orgId: null,
|
|
241
|
+
role: null,
|
|
242
|
+
envAdmin: true,
|
|
243
|
+
canInspectAll: true,
|
|
244
|
+
memberCount: 1,
|
|
245
|
+
},
|
|
246
|
+
sources: [
|
|
247
|
+
{
|
|
248
|
+
id: "current",
|
|
249
|
+
label: "Current Dispatch DB",
|
|
250
|
+
kind: "current",
|
|
251
|
+
current: true,
|
|
252
|
+
connected: true,
|
|
253
|
+
databaseUrlEnv: "DATABASE_URL",
|
|
254
|
+
databaseAuthTokenEnv: null,
|
|
255
|
+
canInspectAll: true,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
mocks.recordAudit.mockResolvedValue(undefined);
|
|
260
|
+
mocks.resourceGetByPath.mockResolvedValue(null);
|
|
261
|
+
mocks.resourceList.mockResolvedValue([]);
|
|
262
|
+
mocks.resourcePut.mockImplementation(
|
|
263
|
+
async (
|
|
264
|
+
owner: string,
|
|
265
|
+
path: string,
|
|
266
|
+
content: string,
|
|
267
|
+
mimeType = "text/markdown",
|
|
268
|
+
) => resourceWithMime(path, content, owner, mimeType),
|
|
269
|
+
);
|
|
270
|
+
mocks.getOrgSetting.mockResolvedValue(null);
|
|
271
|
+
mocks.getUserSetting.mockResolvedValue(null);
|
|
272
|
+
mocks.putOrgSetting.mockResolvedValue(undefined);
|
|
273
|
+
mocks.putUserSetting.mockResolvedValue(undefined);
|
|
274
|
+
mocks.listWorkspaceResources.mockResolvedValue([]);
|
|
275
|
+
mocks.createWorkspaceResource.mockImplementation(async (input) => ({
|
|
276
|
+
id: `workspace-${input.path}`,
|
|
277
|
+
ownerEmail: "owner@example.test",
|
|
278
|
+
orgId: null,
|
|
279
|
+
createdBy: "owner@example.test",
|
|
280
|
+
createdAt: 1,
|
|
281
|
+
updatedAt: 2,
|
|
282
|
+
...input,
|
|
283
|
+
}));
|
|
284
|
+
mocks.updateWorkspaceResource.mockImplementation(
|
|
285
|
+
async (resourceId, input) => ({
|
|
286
|
+
id: resourceId,
|
|
287
|
+
ownerEmail: "owner@example.test",
|
|
288
|
+
orgId: null,
|
|
289
|
+
kind: "instruction",
|
|
290
|
+
path: "instructions/existing.md",
|
|
291
|
+
scope: "all",
|
|
292
|
+
createdBy: "owner@example.test",
|
|
293
|
+
createdAt: 1,
|
|
294
|
+
updatedAt: 2,
|
|
295
|
+
...input,
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
mocks.getDb.mockReturnValue(createDbMock());
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
afterEach(() => {
|
|
302
|
+
vi.clearAllMocks();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("listDreamCandidates", () => {
|
|
306
|
+
it("scores grounded thread signals and keeps per-thread debug errors isolated", async () => {
|
|
307
|
+
mocks.searchAgentThreads.mockResolvedValue({
|
|
308
|
+
source: { id: "current" },
|
|
309
|
+
access: { mode: "local" },
|
|
310
|
+
query: null,
|
|
311
|
+
threads: [{ id: "thread-1" }, { id: "thread-2" }],
|
|
312
|
+
});
|
|
313
|
+
mocks.getAgentThreadDebug.mockImplementation(async ({ threadId }) => {
|
|
314
|
+
if (threadId === "thread-2") {
|
|
315
|
+
throw new Error("debug unavailable");
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
thread: {
|
|
319
|
+
id: "thread-1",
|
|
320
|
+
ownerEmail: "owner@example.test",
|
|
321
|
+
title: "Memory correction",
|
|
322
|
+
preview: "remember this",
|
|
323
|
+
messageCount: 1,
|
|
324
|
+
createdAt: 1,
|
|
325
|
+
updatedAt: 2,
|
|
326
|
+
},
|
|
327
|
+
messages: [
|
|
328
|
+
{
|
|
329
|
+
role: "user",
|
|
330
|
+
text: "Actually, remember to use shadcn DropdownMenu for action menus next time",
|
|
331
|
+
index: 0,
|
|
332
|
+
createdAt: 1,
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
runs: [
|
|
336
|
+
{
|
|
337
|
+
id: "run-1",
|
|
338
|
+
status: "failed",
|
|
339
|
+
abortReason: null,
|
|
340
|
+
events: [{ type: "tool", error: "timed out" }],
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
feedback: [],
|
|
344
|
+
evals: [],
|
|
345
|
+
satisfaction: [],
|
|
346
|
+
checkpoints: [],
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const result = await listDreamCandidates({ limit: 5 });
|
|
351
|
+
|
|
352
|
+
expect(result.candidateCount).toBe(1);
|
|
353
|
+
expect(result.errors).toEqual([
|
|
354
|
+
expect.objectContaining({
|
|
355
|
+
threadId: "thread-2",
|
|
356
|
+
sourceId: "current",
|
|
357
|
+
message: "debug unavailable",
|
|
358
|
+
timedOut: false,
|
|
359
|
+
}),
|
|
360
|
+
]);
|
|
361
|
+
const candidate = result.candidates[0]!;
|
|
362
|
+
expect(candidate.evidenceCounts.rememberRequests).toBe(1);
|
|
363
|
+
expect(candidate.evidenceCounts.explicitCorrections).toBe(1);
|
|
364
|
+
expect(candidate.evidenceCounts.failedRuns).toBe(1);
|
|
365
|
+
expect(candidate.evidenceCounts.toolErrors).toBe(1);
|
|
366
|
+
expect(candidate.reasons.map((entry) => entry.code)).toEqual(
|
|
367
|
+
expect.arrayContaining([
|
|
368
|
+
"remember-request",
|
|
369
|
+
"explicit-correction",
|
|
370
|
+
"failed-run",
|
|
371
|
+
"tool-error",
|
|
372
|
+
]),
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("keeps all-source scans partial when one source times out", async () => {
|
|
377
|
+
mocks.listThreadDebugSources.mockResolvedValue({
|
|
378
|
+
access: {
|
|
379
|
+
viewerEmail: "owner@example.test",
|
|
380
|
+
orgId: null,
|
|
381
|
+
role: null,
|
|
382
|
+
envAdmin: true,
|
|
383
|
+
canInspectAll: true,
|
|
384
|
+
memberCount: 1,
|
|
385
|
+
},
|
|
386
|
+
sources: [
|
|
387
|
+
{
|
|
388
|
+
id: "voice",
|
|
389
|
+
label: "Voice",
|
|
390
|
+
kind: "env",
|
|
391
|
+
current: false,
|
|
392
|
+
connected: true,
|
|
393
|
+
databaseUrlEnv: "VOICE_DATABASE_URL",
|
|
394
|
+
databaseAuthTokenEnv: null,
|
|
395
|
+
canInspectAll: true,
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
id: "mail",
|
|
399
|
+
label: "Mail",
|
|
400
|
+
kind: "env",
|
|
401
|
+
current: false,
|
|
402
|
+
connected: true,
|
|
403
|
+
databaseUrlEnv: "MAIL_DATABASE_URL",
|
|
404
|
+
databaseAuthTokenEnv: null,
|
|
405
|
+
canInspectAll: true,
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
});
|
|
409
|
+
mocks.searchAgentThreads.mockImplementation(async ({ sourceId }) => {
|
|
410
|
+
if (sourceId === "mail") {
|
|
411
|
+
await new Promise(() => undefined);
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
source: {
|
|
415
|
+
id: sourceId,
|
|
416
|
+
label: sourceId === "voice" ? "Voice" : "Mail",
|
|
417
|
+
},
|
|
418
|
+
access: { mode: "local" },
|
|
419
|
+
query: null,
|
|
420
|
+
threads: [{ id: "voice-thread-1" }],
|
|
421
|
+
};
|
|
422
|
+
});
|
|
423
|
+
mocks.getAgentThreadDebug.mockResolvedValue({
|
|
424
|
+
thread: {
|
|
425
|
+
id: "voice-thread-1",
|
|
426
|
+
ownerEmail: "owner@example.test",
|
|
427
|
+
title: "Remember correction",
|
|
428
|
+
preview: "remember",
|
|
429
|
+
messageCount: 1,
|
|
430
|
+
createdAt: 1,
|
|
431
|
+
updatedAt: 2,
|
|
432
|
+
},
|
|
433
|
+
messages: [
|
|
434
|
+
{
|
|
435
|
+
role: "user",
|
|
436
|
+
text: "Remember to keep dream source scans partial.",
|
|
437
|
+
index: 0,
|
|
438
|
+
createdAt: 1,
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
runs: [],
|
|
442
|
+
feedback: [],
|
|
443
|
+
evals: [],
|
|
444
|
+
satisfaction: [],
|
|
445
|
+
checkpoints: [],
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const result = await listDreamCandidates({
|
|
449
|
+
sourceId: "all",
|
|
450
|
+
allSources: true,
|
|
451
|
+
limit: 5,
|
|
452
|
+
sourceTimeoutMs: 5,
|
|
453
|
+
sourceStartStaggerMs: 0,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(result.source).toMatchObject({ id: "all" });
|
|
457
|
+
expect(result.candidateCount).toBe(1);
|
|
458
|
+
expect(result.inspectedThreadCount).toBe(1);
|
|
459
|
+
expect(result.sources).toEqual(
|
|
460
|
+
expect.arrayContaining([
|
|
461
|
+
expect.objectContaining({
|
|
462
|
+
sourceId: "voice",
|
|
463
|
+
status: "ok",
|
|
464
|
+
timeoutMs: 5,
|
|
465
|
+
threadErrorCount: 0,
|
|
466
|
+
inspectedThreadCount: 1,
|
|
467
|
+
candidateCount: 1,
|
|
468
|
+
}),
|
|
469
|
+
expect.objectContaining({
|
|
470
|
+
sourceId: "mail",
|
|
471
|
+
status: "timed_out",
|
|
472
|
+
timeoutMs: 5,
|
|
473
|
+
inspectedThreadCount: 0,
|
|
474
|
+
candidateCount: 0,
|
|
475
|
+
}),
|
|
476
|
+
]),
|
|
477
|
+
);
|
|
478
|
+
expect(result.errors).toEqual(
|
|
479
|
+
expect.arrayContaining([
|
|
480
|
+
expect.objectContaining({
|
|
481
|
+
sourceId: "mail",
|
|
482
|
+
message: "Timed out after 5ms",
|
|
483
|
+
}),
|
|
484
|
+
]),
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("keeps source scans partial when one thread debug read times out", async () => {
|
|
489
|
+
mocks.searchAgentThreads.mockResolvedValue({
|
|
490
|
+
source: { id: "current", label: "Current Dispatch DB" },
|
|
491
|
+
access: { mode: "local" },
|
|
492
|
+
query: null,
|
|
493
|
+
threads: [{ id: "thread-ok" }, { id: "thread-hangs" }],
|
|
494
|
+
});
|
|
495
|
+
mocks.getAgentThreadDebug.mockImplementation(async ({ threadId }) => {
|
|
496
|
+
if (threadId === "thread-hangs") {
|
|
497
|
+
await new Promise(() => undefined);
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
thread: {
|
|
501
|
+
id: "thread-ok",
|
|
502
|
+
ownerEmail: "owner@example.test",
|
|
503
|
+
title: "Remember correction",
|
|
504
|
+
preview: "remember",
|
|
505
|
+
messageCount: 1,
|
|
506
|
+
createdAt: 1,
|
|
507
|
+
updatedAt: 2,
|
|
508
|
+
},
|
|
509
|
+
messages: [
|
|
510
|
+
{
|
|
511
|
+
role: "user",
|
|
512
|
+
text: "Remember that source scans should keep partial thread results.",
|
|
513
|
+
index: 0,
|
|
514
|
+
createdAt: 1,
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
runs: [],
|
|
518
|
+
feedback: [],
|
|
519
|
+
evals: [],
|
|
520
|
+
satisfaction: [],
|
|
521
|
+
checkpoints: [],
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const result = await listDreamCandidates({
|
|
526
|
+
limit: 5,
|
|
527
|
+
sourceTimeoutMs: 50,
|
|
528
|
+
threadTimeoutMs: 5,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(result.candidateCount).toBe(1);
|
|
532
|
+
expect(result.inspectedThreadCount).toBe(2);
|
|
533
|
+
expect(result.errors).toEqual([
|
|
534
|
+
expect.objectContaining({
|
|
535
|
+
threadId: "thread-hangs",
|
|
536
|
+
sourceId: "current",
|
|
537
|
+
timedOut: true,
|
|
538
|
+
message: "Timed out after 5ms",
|
|
539
|
+
}),
|
|
540
|
+
]);
|
|
541
|
+
expect(result.sources[0]).toEqual(
|
|
542
|
+
expect.objectContaining({
|
|
543
|
+
sourceId: "current",
|
|
544
|
+
status: "ok",
|
|
545
|
+
inspectedThreadCount: 2,
|
|
546
|
+
candidateCount: 1,
|
|
547
|
+
threadErrorCount: 1,
|
|
548
|
+
}),
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("does not treat feature wording or successful eval metadata as dream failures", async () => {
|
|
553
|
+
mocks.searchAgentThreads.mockResolvedValue({
|
|
554
|
+
source: { id: "current" },
|
|
555
|
+
access: { mode: "local" },
|
|
556
|
+
query: null,
|
|
557
|
+
threads: [{ id: "thread-1" }],
|
|
558
|
+
});
|
|
559
|
+
mocks.getAgentThreadDebug.mockResolvedValue({
|
|
560
|
+
thread: {
|
|
561
|
+
id: "thread-1",
|
|
562
|
+
ownerEmail: "owner@example.test",
|
|
563
|
+
title: "Create an extension",
|
|
564
|
+
preview: "image instead of camera",
|
|
565
|
+
messageCount: 1,
|
|
566
|
+
createdAt: 1,
|
|
567
|
+
updatedAt: 2,
|
|
568
|
+
},
|
|
569
|
+
messages: [
|
|
570
|
+
{
|
|
571
|
+
role: "user",
|
|
572
|
+
text: "Create an extension to add image instead of camera when recording",
|
|
573
|
+
index: 0,
|
|
574
|
+
createdAt: 1,
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
runs: [
|
|
578
|
+
{
|
|
579
|
+
id: "run-1",
|
|
580
|
+
status: "completed",
|
|
581
|
+
abortReason: null,
|
|
582
|
+
events: [
|
|
583
|
+
{
|
|
584
|
+
event: {
|
|
585
|
+
type: "tool_done",
|
|
586
|
+
tool: "db-query",
|
|
587
|
+
result:
|
|
588
|
+
"query: select status, failure_reason from transcripts\nrows: 1\nstatus | failure_reason\nfailed | no native transcript",
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
feedback: [],
|
|
595
|
+
evals: [
|
|
596
|
+
{
|
|
597
|
+
eval_type: "automated",
|
|
598
|
+
criteria: "tool_success_rate",
|
|
599
|
+
score: 1,
|
|
600
|
+
metadata: JSON.stringify({ failedTools: 0, successfulTools: 1 }),
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
satisfaction: [],
|
|
604
|
+
checkpoints: [],
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const result = await listDreamCandidates({ limit: 5 });
|
|
608
|
+
|
|
609
|
+
expect(result.candidateCount).toBe(0);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("ignores injected context when detecting user corrections", async () => {
|
|
613
|
+
mocks.searchAgentThreads.mockResolvedValue({
|
|
614
|
+
source: { id: "videos" },
|
|
615
|
+
access: { mode: "local" },
|
|
616
|
+
query: null,
|
|
617
|
+
threads: [{ id: "thread-context" }],
|
|
618
|
+
});
|
|
619
|
+
mocks.getAgentThreadDebug.mockResolvedValue({
|
|
620
|
+
thread: {
|
|
621
|
+
id: "thread-context",
|
|
622
|
+
ownerEmail: "owner@example.test",
|
|
623
|
+
title: "einstein shaking head",
|
|
624
|
+
preview: "composition request",
|
|
625
|
+
messageCount: 1,
|
|
626
|
+
createdAt: 1,
|
|
627
|
+
updatedAt: 2,
|
|
628
|
+
},
|
|
629
|
+
messages: [
|
|
630
|
+
{
|
|
631
|
+
role: "user",
|
|
632
|
+
text: "einstein shaking head\n\n<context>\nUse the Videos app flow. Do not route this as source-code generation.</context>",
|
|
633
|
+
index: 0,
|
|
634
|
+
createdAt: 1,
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
runs: [],
|
|
638
|
+
feedback: [],
|
|
639
|
+
evals: [],
|
|
640
|
+
satisfaction: [],
|
|
641
|
+
checkpoints: [],
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const result = await listDreamCandidates({
|
|
645
|
+
sourceId: "videos",
|
|
646
|
+
limit: 5,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
expect(result.candidateCount).toBe(0);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
describe("buildProposalInputs", () => {
|
|
654
|
+
it("skips personal memory proposals whose source evidence is already captured", () => {
|
|
655
|
+
const result = buildProposalInputs(
|
|
656
|
+
[candidateWithEvidence([explicitEvidence()])],
|
|
657
|
+
{
|
|
658
|
+
personalIndex: "# Memory Index\n",
|
|
659
|
+
personalNotes: [
|
|
660
|
+
{
|
|
661
|
+
path: "memory/ui.md",
|
|
662
|
+
content:
|
|
663
|
+
"Use shadcn DropdownMenu for action menus.\n\nSource thread: thread-1",
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
sharedLearnings: "",
|
|
667
|
+
},
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
expect(result.proposals).toEqual([]);
|
|
671
|
+
expect(result.guardrailNotes.join("\n")).toContain("Skipped duplicate");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("uses the personal memory index as part of duplicate detection", () => {
|
|
675
|
+
const result = buildProposalInputs(
|
|
676
|
+
[candidateWithEvidence([explicitEvidence()])],
|
|
677
|
+
{
|
|
678
|
+
personalIndex:
|
|
679
|
+
"# Memory Index\n\n- [ui](ui.md) — Source thread: thread-1\n",
|
|
680
|
+
personalNotes: [],
|
|
681
|
+
sharedLearnings: "",
|
|
682
|
+
},
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
expect(result.proposals).toEqual([]);
|
|
686
|
+
expect(result.guardrailNotes.join("\n")).toContain("Skipped duplicate");
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("retargets likely stale personal memories instead of creating parallel notes", () => {
|
|
690
|
+
const result = buildProposalInputs(
|
|
691
|
+
[candidateWithEvidence([explicitEvidence()])],
|
|
692
|
+
{
|
|
693
|
+
personalIndex: "# Memory Index\n",
|
|
694
|
+
personalNotes: [
|
|
695
|
+
{
|
|
696
|
+
path: "memory/ui-patterns.md",
|
|
697
|
+
content: "Use shadcn DropdownMenu for action menus.",
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
sharedLearnings: "",
|
|
701
|
+
},
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
expect(result.proposals).toHaveLength(1);
|
|
705
|
+
expect(result.proposals[0]).toMatchObject({
|
|
706
|
+
targetType: "personal-memory",
|
|
707
|
+
targetPath: "memory/ui-patterns.md",
|
|
708
|
+
title: "Update existing memory from recent corrections",
|
|
709
|
+
});
|
|
710
|
+
expect(result.guardrailNotes.join("\n")).toContain("Retargeted proposal");
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("skips shared learning proposals already captured in LEARNINGS.md", () => {
|
|
714
|
+
const failureEvidence: DreamEvidence[] = [
|
|
715
|
+
{
|
|
716
|
+
kind: "failed-run",
|
|
717
|
+
label: "Run failed or aborted",
|
|
718
|
+
snippet: "tool timed out while syncing workspace resources",
|
|
719
|
+
threadId: "thread-a",
|
|
720
|
+
threadTitle: "Sync A",
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
kind: "tool-error",
|
|
724
|
+
label: "Tool call reported an error",
|
|
725
|
+
snippet: "tool timed out while syncing workspace resources",
|
|
726
|
+
threadId: "thread-b",
|
|
727
|
+
threadTitle: "Sync B",
|
|
728
|
+
},
|
|
729
|
+
];
|
|
730
|
+
|
|
731
|
+
const result = buildProposalInputs(
|
|
732
|
+
[
|
|
733
|
+
candidateWithEvidence([failureEvidence[0]!]),
|
|
734
|
+
candidateWithEvidence([failureEvidence[1]!]),
|
|
735
|
+
],
|
|
736
|
+
{
|
|
737
|
+
personalIndex: "",
|
|
738
|
+
personalNotes: [],
|
|
739
|
+
sharedLearnings:
|
|
740
|
+
"# Learnings\n\n## Patterns\n\nSource threads: thread-a, thread-b\n",
|
|
741
|
+
},
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
expect(result.proposals).toEqual([]);
|
|
745
|
+
expect(result.guardrailNotes.join("\n")).toContain("Skipped duplicate");
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("routes personal-memory proposals to shared learnings when the owner scope is not personal", () => {
|
|
749
|
+
const result = buildProposalInputs(
|
|
750
|
+
[
|
|
751
|
+
candidateWithEvidence([explicitEvidence({ threadId: "thread-1" })]),
|
|
752
|
+
candidateWithEvidence(
|
|
753
|
+
[
|
|
754
|
+
explicitEvidence({
|
|
755
|
+
threadId: "thread-2",
|
|
756
|
+
snippet: "Remember to keep dream proposals reviewable",
|
|
757
|
+
}),
|
|
758
|
+
],
|
|
759
|
+
"mail",
|
|
760
|
+
),
|
|
761
|
+
],
|
|
762
|
+
{
|
|
763
|
+
personalIndex: "# Memory Index\n",
|
|
764
|
+
personalNotes: [],
|
|
765
|
+
sharedLearnings: "",
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
personalMemoryAllowed: false,
|
|
769
|
+
personalMemoryBlockReason:
|
|
770
|
+
"source evidence includes a thread owned by another user",
|
|
771
|
+
},
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
expect(result.proposals).toHaveLength(1);
|
|
775
|
+
expect(result.proposals[0]).toMatchObject({
|
|
776
|
+
targetType: "shared-learnings",
|
|
777
|
+
targetPath: "LEARNINGS.md",
|
|
778
|
+
risk: "medium",
|
|
779
|
+
});
|
|
780
|
+
expect(result.proposals[0]?.content).toContain(
|
|
781
|
+
"Provenance: Personal memory was disabled",
|
|
782
|
+
);
|
|
783
|
+
expect(result.guardrailNotes.join("\n")).toContain(
|
|
784
|
+
"Routed personal-memory dream proposals to shared learnings",
|
|
785
|
+
);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("proposes workspace instructions from repeated corrections", () => {
|
|
789
|
+
const result = buildProposalInputs(
|
|
790
|
+
[
|
|
791
|
+
candidateWithEvidence([
|
|
792
|
+
explicitEvidence({
|
|
793
|
+
threadId: "thread-1",
|
|
794
|
+
snippet: "Actually use actions first",
|
|
795
|
+
}),
|
|
796
|
+
]),
|
|
797
|
+
candidateWithEvidence([
|
|
798
|
+
explicitEvidence({
|
|
799
|
+
threadId: "thread-2",
|
|
800
|
+
snippet:
|
|
801
|
+
"Remember to use workspace resources for shared instructions",
|
|
802
|
+
}),
|
|
803
|
+
explicitEvidence({
|
|
804
|
+
threadId: "thread-2",
|
|
805
|
+
snippet: "From now on, keep dream proposals reviewable",
|
|
806
|
+
}),
|
|
807
|
+
]),
|
|
808
|
+
],
|
|
809
|
+
{
|
|
810
|
+
personalIndex: "",
|
|
811
|
+
personalNotes: [],
|
|
812
|
+
sharedLearnings: "",
|
|
813
|
+
},
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
expect(result.proposals).toEqual(
|
|
817
|
+
expect.arrayContaining([
|
|
818
|
+
expect.objectContaining({
|
|
819
|
+
targetType: "workspace-instruction",
|
|
820
|
+
targetPath: expect.stringMatching(
|
|
821
|
+
/^instructions\/dream-corrections-/,
|
|
822
|
+
),
|
|
823
|
+
risk: "medium",
|
|
824
|
+
}),
|
|
825
|
+
]),
|
|
826
|
+
);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it("deduplicates repeated evidence and keeps single-app UI wording out of global instructions", () => {
|
|
830
|
+
const repeatedCorrection = explicitEvidence({
|
|
831
|
+
threadId: "thread-clips",
|
|
832
|
+
threadTitle: "Clips export copy",
|
|
833
|
+
snippet: "Actually the Clips UI button label should say Export",
|
|
834
|
+
sourceId: "clips",
|
|
835
|
+
});
|
|
836
|
+
const result = buildProposalInputs(
|
|
837
|
+
[
|
|
838
|
+
candidateWithEvidence(
|
|
839
|
+
[
|
|
840
|
+
repeatedCorrection,
|
|
841
|
+
{
|
|
842
|
+
...repeatedCorrection,
|
|
843
|
+
messageIndex: 1,
|
|
844
|
+
},
|
|
845
|
+
],
|
|
846
|
+
"clips",
|
|
847
|
+
),
|
|
848
|
+
candidateWithEvidence(
|
|
849
|
+
[
|
|
850
|
+
explicitEvidence({
|
|
851
|
+
threadId: "thread-clips-2",
|
|
852
|
+
threadTitle: "Clips export wording",
|
|
853
|
+
snippet:
|
|
854
|
+
"Remember the Clips button wording should use Export in this screen",
|
|
855
|
+
sourceId: "clips",
|
|
856
|
+
}),
|
|
857
|
+
],
|
|
858
|
+
"clips",
|
|
859
|
+
),
|
|
860
|
+
],
|
|
861
|
+
{
|
|
862
|
+
personalIndex: "",
|
|
863
|
+
personalNotes: [],
|
|
864
|
+
sharedLearnings: "",
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
personalMemoryAllowed: false,
|
|
868
|
+
personalMemoryBlockReason: "admin scan spans shared production data",
|
|
869
|
+
},
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
const shared = result.proposals.find(
|
|
873
|
+
(proposal) => proposal.targetType === "shared-learnings",
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
expect(shared?.evidence).toHaveLength(2);
|
|
877
|
+
expect(
|
|
878
|
+
shared?.content.match(/Actually the Clips UI button label/g),
|
|
879
|
+
).toHaveLength(1);
|
|
880
|
+
expect(
|
|
881
|
+
result.proposals.some(
|
|
882
|
+
(proposal) => proposal.targetType === "workspace-instruction",
|
|
883
|
+
),
|
|
884
|
+
).toBe(false);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it("summarizes raw eval failure rows before writing proposal content", () => {
|
|
888
|
+
const result = buildProposalInputs(
|
|
889
|
+
[
|
|
890
|
+
candidateWithEvidence(
|
|
891
|
+
[
|
|
892
|
+
{
|
|
893
|
+
kind: "failed-run",
|
|
894
|
+
label: "Run failed or aborted",
|
|
895
|
+
snippet: "failed: Notion import exceeded retry budget",
|
|
896
|
+
threadId: "thread-a",
|
|
897
|
+
threadTitle: "Slow dream run A",
|
|
898
|
+
runId: "run-a",
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
kind: "eval-failure",
|
|
902
|
+
label: "Evaluation failed or scored low",
|
|
903
|
+
snippet: {
|
|
904
|
+
name: "latency_score",
|
|
905
|
+
score: 0,
|
|
906
|
+
passed: false,
|
|
907
|
+
run_id: "run-a",
|
|
908
|
+
metadata: JSON.stringify({
|
|
909
|
+
actualMs: 61_250,
|
|
910
|
+
expectedMs: 30_000,
|
|
911
|
+
}),
|
|
912
|
+
} as any,
|
|
913
|
+
threadId: "thread-a",
|
|
914
|
+
threadTitle: "Slow dream run A",
|
|
915
|
+
runId: "run-a",
|
|
916
|
+
},
|
|
917
|
+
],
|
|
918
|
+
"dispatch-prod",
|
|
919
|
+
),
|
|
920
|
+
candidateWithEvidence(
|
|
921
|
+
[
|
|
922
|
+
{
|
|
923
|
+
kind: "failed-run",
|
|
924
|
+
label: "Run failed or aborted",
|
|
925
|
+
snippet: "failed: Notion import exceeded retry budget",
|
|
926
|
+
threadId: "thread-b",
|
|
927
|
+
threadTitle: "Slow dream run B",
|
|
928
|
+
runId: "run-b",
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
kind: "eval-failure",
|
|
932
|
+
label: "Evaluation failed or scored low",
|
|
933
|
+
snippet: {
|
|
934
|
+
name: "latency_score",
|
|
935
|
+
score: 0.2,
|
|
936
|
+
passed: false,
|
|
937
|
+
run_id: "run-b",
|
|
938
|
+
metadata: JSON.stringify({
|
|
939
|
+
actualMs: 44_000,
|
|
940
|
+
expectedMs: 30_000,
|
|
941
|
+
}),
|
|
942
|
+
} as any,
|
|
943
|
+
threadId: "thread-b",
|
|
944
|
+
threadTitle: "Slow dream run B",
|
|
945
|
+
runId: "run-b",
|
|
946
|
+
},
|
|
947
|
+
],
|
|
948
|
+
"analytics-prod",
|
|
949
|
+
),
|
|
950
|
+
],
|
|
951
|
+
{
|
|
952
|
+
personalIndex: "",
|
|
953
|
+
personalNotes: [],
|
|
954
|
+
sharedLearnings: "",
|
|
955
|
+
},
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
const content = result.proposals
|
|
959
|
+
.map((proposal) => proposal.content)
|
|
960
|
+
.join("\n");
|
|
961
|
+
|
|
962
|
+
expect(content).toContain("latency score failed");
|
|
963
|
+
expect(content).toContain("actual 61250ms, expected 30000ms");
|
|
964
|
+
expect(content).not.toContain('"metadata"');
|
|
965
|
+
expect(content).not.toContain('"run_id"');
|
|
966
|
+
expect(content).not.toContain("{");
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it("does not create durable proposals from eval-only or account setup failures", () => {
|
|
970
|
+
const result = buildProposalInputs(
|
|
971
|
+
[
|
|
972
|
+
candidateWithEvidence(
|
|
973
|
+
[
|
|
974
|
+
{
|
|
975
|
+
kind: "eval-failure",
|
|
976
|
+
label: "Evaluation failed or scored low",
|
|
977
|
+
snippet: {
|
|
978
|
+
name: "cost_efficiency",
|
|
979
|
+
score: 0,
|
|
980
|
+
passed: false,
|
|
981
|
+
metadata: JSON.stringify({
|
|
982
|
+
actualCx100: 4160,
|
|
983
|
+
expectedCx100: 300,
|
|
984
|
+
}),
|
|
985
|
+
} as any,
|
|
986
|
+
threadId: "thread-a",
|
|
987
|
+
threadTitle: "Costly run",
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
kind: "tool-error",
|
|
991
|
+
label: "Tool call reported an error",
|
|
992
|
+
snippet:
|
|
993
|
+
"Tool error (credits-limit-daily): You've reached the daily AI credits limit for your current plan.",
|
|
994
|
+
threadId: "thread-a",
|
|
995
|
+
threadTitle: "Costly run",
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
"mail",
|
|
999
|
+
),
|
|
1000
|
+
candidateWithEvidence(
|
|
1001
|
+
[
|
|
1002
|
+
{
|
|
1003
|
+
kind: "eval-failure",
|
|
1004
|
+
label: "Evaluation failed or scored low",
|
|
1005
|
+
snippet: {
|
|
1006
|
+
name: "latency_score",
|
|
1007
|
+
score: 0,
|
|
1008
|
+
passed: false,
|
|
1009
|
+
metadata: JSON.stringify({
|
|
1010
|
+
actual_ms: 61_250,
|
|
1011
|
+
expected_ms: 30_000,
|
|
1012
|
+
}),
|
|
1013
|
+
} as any,
|
|
1014
|
+
threadId: "thread-b",
|
|
1015
|
+
threadTitle: "Slow run",
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
kind: "tool-error",
|
|
1019
|
+
label: "Tool call reported an error",
|
|
1020
|
+
snippet:
|
|
1021
|
+
"Tool error (missing_credentials): No LLM provider is connected.",
|
|
1022
|
+
threadId: "thread-b",
|
|
1023
|
+
threadTitle: "Slow run",
|
|
1024
|
+
},
|
|
1025
|
+
],
|
|
1026
|
+
"content",
|
|
1027
|
+
),
|
|
1028
|
+
],
|
|
1029
|
+
{
|
|
1030
|
+
personalIndex: "",
|
|
1031
|
+
personalNotes: [],
|
|
1032
|
+
sharedLearnings: "",
|
|
1033
|
+
},
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
expect(result.proposals).toEqual([]);
|
|
1037
|
+
expect(result.guardrailNotes.join("\n")).toContain(
|
|
1038
|
+
"Skipped failure proposals because the signals were eval-only noise",
|
|
1039
|
+
);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it("explains why admin-visible one-off corrections produce no proposal", () => {
|
|
1043
|
+
const result = buildProposalInputs(
|
|
1044
|
+
[
|
|
1045
|
+
candidateWithEvidence(
|
|
1046
|
+
[
|
|
1047
|
+
explicitEvidence({
|
|
1048
|
+
threadId: "thread-forms",
|
|
1049
|
+
threadTitle: "Recording answer",
|
|
1050
|
+
snippet:
|
|
1051
|
+
"Actually this Forms screen should say record instead of upload",
|
|
1052
|
+
sourceId: "forms",
|
|
1053
|
+
}),
|
|
1054
|
+
],
|
|
1055
|
+
"forms",
|
|
1056
|
+
),
|
|
1057
|
+
],
|
|
1058
|
+
{
|
|
1059
|
+
personalIndex: "",
|
|
1060
|
+
personalNotes: [],
|
|
1061
|
+
sharedLearnings: "",
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
personalMemoryAllowed: false,
|
|
1065
|
+
personalMemoryBlockReason:
|
|
1066
|
+
"source evidence includes a thread owned by another user",
|
|
1067
|
+
},
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
expect(result.proposals).toEqual([]);
|
|
1071
|
+
const notes = result.guardrailNotes.join("\n");
|
|
1072
|
+
expect(notes).toContain(
|
|
1073
|
+
"Skipped explicit user-correction proposals because personal memory is blocked",
|
|
1074
|
+
);
|
|
1075
|
+
expect(notes).toContain(
|
|
1076
|
+
"Skipped workspace-instruction promotion for explicit corrections",
|
|
1077
|
+
);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it("requires workspace instruction evidence to span threads or source apps", () => {
|
|
1081
|
+
const sameThreadFailures: DreamEvidence[] = [
|
|
1082
|
+
{
|
|
1083
|
+
kind: "failed-run",
|
|
1084
|
+
label: "Run failed or aborted",
|
|
1085
|
+
snippet: "failed: resource sync wrote an invalid workspace resource",
|
|
1086
|
+
threadId: "thread-a",
|
|
1087
|
+
threadTitle: "Production timeout",
|
|
1088
|
+
sourceId: "dispatch-prod",
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
kind: "tool-error",
|
|
1092
|
+
label: "Tool call reported an error",
|
|
1093
|
+
snippet:
|
|
1094
|
+
"Tool error (schema-mismatch): resource grant failed validation",
|
|
1095
|
+
threadId: "thread-a",
|
|
1096
|
+
threadTitle: "Production timeout",
|
|
1097
|
+
sourceId: "dispatch-prod",
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
kind: "eval-failure",
|
|
1101
|
+
label: "Evaluation failed or scored low",
|
|
1102
|
+
snippet: "latency score failed; score 0",
|
|
1103
|
+
threadId: "thread-a",
|
|
1104
|
+
threadTitle: "Production timeout",
|
|
1105
|
+
sourceId: "dispatch-prod",
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
kind: "frustration",
|
|
1109
|
+
label: "User expressed friction or repeated failure",
|
|
1110
|
+
snippet: "This keeps failing again",
|
|
1111
|
+
threadId: "thread-a",
|
|
1112
|
+
threadTitle: "Production timeout",
|
|
1113
|
+
sourceId: "dispatch-prod",
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
kind: "negative-feedback",
|
|
1117
|
+
label: "Negative feedback was recorded",
|
|
1118
|
+
snippet: "User said the workspace sync fix did not work",
|
|
1119
|
+
threadId: "thread-a",
|
|
1120
|
+
threadTitle: "Production timeout",
|
|
1121
|
+
sourceId: "dispatch-prod",
|
|
1122
|
+
},
|
|
1123
|
+
];
|
|
1124
|
+
|
|
1125
|
+
const singleSource = buildProposalInputs(
|
|
1126
|
+
[candidateWithEvidence(sameThreadFailures, "dispatch-prod")],
|
|
1127
|
+
{
|
|
1128
|
+
personalIndex: "",
|
|
1129
|
+
personalNotes: [],
|
|
1130
|
+
sharedLearnings: "",
|
|
1131
|
+
},
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
expect(
|
|
1135
|
+
singleSource.proposals.some(
|
|
1136
|
+
(proposal) => proposal.targetType === "workspace-instruction",
|
|
1137
|
+
),
|
|
1138
|
+
).toBe(false);
|
|
1139
|
+
|
|
1140
|
+
const crossSource = buildProposalInputs(
|
|
1141
|
+
[
|
|
1142
|
+
candidateWithEvidence(sameThreadFailures.slice(0, 2), "dispatch-prod"),
|
|
1143
|
+
candidateWithEvidence(
|
|
1144
|
+
sameThreadFailures.slice(2).map((entry) => ({
|
|
1145
|
+
...entry,
|
|
1146
|
+
sourceId: "analytics-prod",
|
|
1147
|
+
})),
|
|
1148
|
+
"analytics-prod",
|
|
1149
|
+
),
|
|
1150
|
+
],
|
|
1151
|
+
{
|
|
1152
|
+
personalIndex: "",
|
|
1153
|
+
personalNotes: [],
|
|
1154
|
+
sharedLearnings: "",
|
|
1155
|
+
},
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
expect(crossSource.proposals).toEqual(
|
|
1159
|
+
expect.arrayContaining([
|
|
1160
|
+
expect.objectContaining({
|
|
1161
|
+
targetType: "workspace-instruction",
|
|
1162
|
+
targetPath: expect.stringMatching(
|
|
1163
|
+
/^instructions\/dream-run-reliability-/,
|
|
1164
|
+
),
|
|
1165
|
+
}),
|
|
1166
|
+
]),
|
|
1167
|
+
);
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
describe("applyDreamProposal", () => {
|
|
1172
|
+
it("previews workspace proposal target content and approval behavior", async () => {
|
|
1173
|
+
mocks.getApprovalPolicy.mockResolvedValue({
|
|
1174
|
+
enabled: true,
|
|
1175
|
+
approverEmails: ["admin@example.test"],
|
|
1176
|
+
});
|
|
1177
|
+
mocks.getDb.mockReturnValue(
|
|
1178
|
+
createDbMock(
|
|
1179
|
+
pendingProposal({
|
|
1180
|
+
targetType: "workspace-instruction",
|
|
1181
|
+
targetPath: "instructions/dream-corrections.md",
|
|
1182
|
+
title: "Create workspace instruction from repeated corrections",
|
|
1183
|
+
summary:
|
|
1184
|
+
"Repeated corrections should become a workspace instruction.",
|
|
1185
|
+
content: "# Proposed instruction\n\nUse reviewed guidance.",
|
|
1186
|
+
}),
|
|
1187
|
+
),
|
|
1188
|
+
);
|
|
1189
|
+
mocks.listWorkspaceResources.mockResolvedValue([
|
|
1190
|
+
{
|
|
1191
|
+
id: "workspace-existing",
|
|
1192
|
+
ownerEmail: "owner@example.test",
|
|
1193
|
+
orgId: null,
|
|
1194
|
+
kind: "instruction",
|
|
1195
|
+
name: "Existing instruction",
|
|
1196
|
+
description: "Existing",
|
|
1197
|
+
path: "instructions/dream-corrections.md",
|
|
1198
|
+
content: "# Existing instruction\n",
|
|
1199
|
+
scope: "all",
|
|
1200
|
+
createdBy: "owner@example.test",
|
|
1201
|
+
createdAt: 1,
|
|
1202
|
+
updatedAt: 2,
|
|
1203
|
+
},
|
|
1204
|
+
]);
|
|
1205
|
+
|
|
1206
|
+
const preview = await previewDreamProposal("proposal-1");
|
|
1207
|
+
|
|
1208
|
+
expect(preview).toMatchObject({
|
|
1209
|
+
operation: "update",
|
|
1210
|
+
targetExists: true,
|
|
1211
|
+
currentContent: "# Existing instruction\n",
|
|
1212
|
+
proposedContent: expect.stringContaining("Proposed instruction"),
|
|
1213
|
+
target: {
|
|
1214
|
+
kind: "instruction",
|
|
1215
|
+
resourceId: "workspace-existing",
|
|
1216
|
+
path: "instructions/dream-corrections.md",
|
|
1217
|
+
},
|
|
1218
|
+
approval: {
|
|
1219
|
+
required: true,
|
|
1220
|
+
policyEnabled: true,
|
|
1221
|
+
willRequestApproval: true,
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
it("writes personal memory and updates the memory index before auditing", async () => {
|
|
1227
|
+
mocks.getDb.mockReturnValue(createDbMock(pendingProposal()));
|
|
1228
|
+
mocks.resourceGetByPath.mockImplementation(async (_owner, path) => {
|
|
1229
|
+
if (path === "memory/MEMORY.md") {
|
|
1230
|
+
return resource("memory/MEMORY.md", "# Memory Index\n");
|
|
1231
|
+
}
|
|
1232
|
+
return null;
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
const result = await applyDreamProposal("proposal-1");
|
|
1236
|
+
|
|
1237
|
+
expect(mocks.resourcePut).toHaveBeenCalledWith(
|
|
1238
|
+
"owner@example.test",
|
|
1239
|
+
"memory/custom.md",
|
|
1240
|
+
expect.stringContaining("## Provenance"),
|
|
1241
|
+
"text/markdown",
|
|
1242
|
+
expect.objectContaining({
|
|
1243
|
+
createdBy: "agent",
|
|
1244
|
+
metadata: { dreamId: "dream-1", proposalId: "proposal-1" },
|
|
1245
|
+
}),
|
|
1246
|
+
);
|
|
1247
|
+
expect(mocks.resourcePut).toHaveBeenCalledWith(
|
|
1248
|
+
"owner@example.test",
|
|
1249
|
+
"memory/MEMORY.md",
|
|
1250
|
+
expect.stringContaining("[custom](custom.md)"),
|
|
1251
|
+
"text/markdown",
|
|
1252
|
+
expect.any(Object),
|
|
1253
|
+
);
|
|
1254
|
+
expect(result.proposal.status).toBe("applied");
|
|
1255
|
+
expect(mocks.recordAudit).toHaveBeenCalledWith(
|
|
1256
|
+
expect.objectContaining({
|
|
1257
|
+
action: "dream.proposal.applied",
|
|
1258
|
+
targetId: "proposal-1",
|
|
1259
|
+
}),
|
|
1260
|
+
);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
it("rejects personal memory proposals owned by another user", async () => {
|
|
1264
|
+
mocks.getDb.mockReturnValue(
|
|
1265
|
+
createDbMock(pendingProposal({ ownerEmail: "other@example.test" })),
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
await expect(applyDreamProposal("proposal-1")).rejects.toThrow(
|
|
1269
|
+
"Personal memory proposals can only be applied by owner",
|
|
1270
|
+
);
|
|
1271
|
+
expect(mocks.resourcePut).not.toHaveBeenCalled();
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it("queues shared proposals for approval when approval policy is enabled", async () => {
|
|
1275
|
+
mocks.getApprovalPolicy.mockResolvedValue({
|
|
1276
|
+
enabled: true,
|
|
1277
|
+
approverEmails: ["admin@example.test"],
|
|
1278
|
+
});
|
|
1279
|
+
mocks.getDb.mockReturnValue(
|
|
1280
|
+
createDbMock(
|
|
1281
|
+
pendingProposal({
|
|
1282
|
+
targetType: "shared-learnings",
|
|
1283
|
+
targetPath: "LEARNINGS.md",
|
|
1284
|
+
}),
|
|
1285
|
+
),
|
|
1286
|
+
);
|
|
1287
|
+
|
|
1288
|
+
const result = await applyDreamProposal("proposal-1");
|
|
1289
|
+
|
|
1290
|
+
expect(mocks.createApprovalRequest).toHaveBeenCalledWith(
|
|
1291
|
+
expect.objectContaining({
|
|
1292
|
+
changeType: "dream-proposal.apply",
|
|
1293
|
+
targetType: "dream-proposal",
|
|
1294
|
+
targetId: "proposal-1",
|
|
1295
|
+
payload: { proposalId: "proposal-1" },
|
|
1296
|
+
}),
|
|
1297
|
+
);
|
|
1298
|
+
expect(mocks.resourcePut).not.toHaveBeenCalled();
|
|
1299
|
+
expect(result.proposal.status).toBe("approval_requested");
|
|
1300
|
+
expect(result.result).toEqual({
|
|
1301
|
+
approvalRequired: true,
|
|
1302
|
+
approvalId: "approval-1",
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
it("applies approved shared proposals to LEARNINGS.md", async () => {
|
|
1307
|
+
mocks.getDb.mockReturnValue(
|
|
1308
|
+
createDbMock(
|
|
1309
|
+
pendingProposal({
|
|
1310
|
+
status: "approval_requested",
|
|
1311
|
+
targetType: "shared-learnings",
|
|
1312
|
+
targetPath: "LEARNINGS.md",
|
|
1313
|
+
summary: "Record a repeated Dispatch failure pattern.",
|
|
1314
|
+
}),
|
|
1315
|
+
),
|
|
1316
|
+
);
|
|
1317
|
+
mocks.resourceGetByPath.mockResolvedValue(
|
|
1318
|
+
resource("LEARNINGS.md", "# Learnings\n\n## Patterns\n", "__shared__"),
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
const result = await applyApprovedDreamProposal(
|
|
1322
|
+
"proposal-1",
|
|
1323
|
+
"admin@example.test",
|
|
1324
|
+
{ ownerEmail: "owner@example.test", orgId: null },
|
|
1325
|
+
);
|
|
1326
|
+
|
|
1327
|
+
expect(mocks.resourcePut).toHaveBeenCalledWith(
|
|
1328
|
+
"__shared__",
|
|
1329
|
+
"LEARNINGS.md",
|
|
1330
|
+
expect.stringContaining("Record a repeated Dispatch failure pattern."),
|
|
1331
|
+
"text/markdown",
|
|
1332
|
+
expect.objectContaining({
|
|
1333
|
+
createdBy: "agent",
|
|
1334
|
+
metadata: { dreamId: "dream-1", proposalId: "proposal-1" },
|
|
1335
|
+
}),
|
|
1336
|
+
);
|
|
1337
|
+
expect(result.proposal.status).toBe("applied");
|
|
1338
|
+
expect(mocks.recordAudit).toHaveBeenCalledWith(
|
|
1339
|
+
expect.objectContaining({
|
|
1340
|
+
action: "dream.proposal.applied",
|
|
1341
|
+
actor: "admin@example.test",
|
|
1342
|
+
}),
|
|
1343
|
+
);
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
it("creates all-scope workspace resources for approved workspace proposals", async () => {
|
|
1347
|
+
mocks.getDb.mockReturnValue(
|
|
1348
|
+
createDbMock(
|
|
1349
|
+
pendingProposal({
|
|
1350
|
+
status: "approval_requested",
|
|
1351
|
+
targetType: "workspace-instruction",
|
|
1352
|
+
targetPath: "instructions/dream-corrections.md",
|
|
1353
|
+
title: "Create workspace instruction from repeated corrections",
|
|
1354
|
+
summary:
|
|
1355
|
+
"Repeated corrections should become a workspace instruction.",
|
|
1356
|
+
content: "# Dream instruction\n\nUse reviewed workspace guidance.",
|
|
1357
|
+
}),
|
|
1358
|
+
),
|
|
1359
|
+
);
|
|
1360
|
+
|
|
1361
|
+
const result = await applyApprovedDreamProposal(
|
|
1362
|
+
"proposal-1",
|
|
1363
|
+
"admin@example.test",
|
|
1364
|
+
{ ownerEmail: "owner@example.test", orgId: null },
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
expect(mocks.createWorkspaceResource).toHaveBeenCalledWith(
|
|
1368
|
+
expect.objectContaining({
|
|
1369
|
+
kind: "instruction",
|
|
1370
|
+
path: "instructions/dream-corrections.md",
|
|
1371
|
+
scope: "all",
|
|
1372
|
+
content: expect.stringContaining("Dream instruction"),
|
|
1373
|
+
}),
|
|
1374
|
+
);
|
|
1375
|
+
expect(result.proposal.status).toBe("applied");
|
|
1376
|
+
expect(mocks.recordAudit).toHaveBeenCalledWith(
|
|
1377
|
+
expect.objectContaining({
|
|
1378
|
+
action: "dream.proposal.applied",
|
|
1379
|
+
actor: "admin@example.test",
|
|
1380
|
+
}),
|
|
1381
|
+
);
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
describe("ensureDreamJob", () => {
|
|
1386
|
+
it("materializes the recurring dream job with schedule metadata", async () => {
|
|
1387
|
+
const result = await ensureDreamJob({
|
|
1388
|
+
schedule: "0 10 * * 2",
|
|
1389
|
+
sourceId: "current",
|
|
1390
|
+
query: "memory",
|
|
1391
|
+
limit: 12,
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
expect(mocks.resourcePut).toHaveBeenCalledWith(
|
|
1395
|
+
"owner@example.test",
|
|
1396
|
+
"jobs/dispatch-dream.md",
|
|
1397
|
+
expect.stringContaining('schedule: "0 10 * * 2"'),
|
|
1398
|
+
"text/markdown",
|
|
1399
|
+
expect.objectContaining({
|
|
1400
|
+
createdBy: "agent",
|
|
1401
|
+
metadata: expect.objectContaining({
|
|
1402
|
+
sourceId: "current",
|
|
1403
|
+
query: "memory",
|
|
1404
|
+
limit: 12,
|
|
1405
|
+
sourceTimeoutMs: 30000,
|
|
1406
|
+
}),
|
|
1407
|
+
}),
|
|
1408
|
+
);
|
|
1409
|
+
expect(result).toMatchObject({
|
|
1410
|
+
path: "jobs/dispatch-dream.md",
|
|
1411
|
+
schedule: "0 10 * * 2",
|
|
1412
|
+
runAs: "creator",
|
|
1413
|
+
sourceId: "current",
|
|
1414
|
+
query: "memory",
|
|
1415
|
+
limit: 12,
|
|
1416
|
+
});
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
it("rejects invalid cron before writing the job resource", async () => {
|
|
1420
|
+
await expect(ensureDreamJob({ schedule: "weekly please" })).rejects.toThrow(
|
|
1421
|
+
"Invalid cron expression",
|
|
1422
|
+
);
|
|
1423
|
+
expect(mocks.resourcePut).not.toHaveBeenCalled();
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it("persists recurring dream settings", async () => {
|
|
1427
|
+
mocks.getUserSetting.mockResolvedValueOnce(null).mockResolvedValueOnce({
|
|
1428
|
+
enabled: true,
|
|
1429
|
+
schedule: "0 8 * * 1",
|
|
1430
|
+
sourceId: "all",
|
|
1431
|
+
allSources: true,
|
|
1432
|
+
limit: 9,
|
|
1433
|
+
sourceTimeoutMs: 45000,
|
|
1434
|
+
minCandidateCount: 3,
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
const result = await setDreamSettings({
|
|
1438
|
+
enabled: true,
|
|
1439
|
+
schedule: "0 8 * * 1",
|
|
1440
|
+
sourceId: "all",
|
|
1441
|
+
allSources: true,
|
|
1442
|
+
limit: 9,
|
|
1443
|
+
sourceTimeoutMs: 45000,
|
|
1444
|
+
minCandidateCount: 3,
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
expect(mocks.putUserSetting).toHaveBeenCalledWith(
|
|
1448
|
+
"owner@example.test",
|
|
1449
|
+
"dispatch-dream-settings",
|
|
1450
|
+
expect.objectContaining({
|
|
1451
|
+
enabled: true,
|
|
1452
|
+
schedule: "0 8 * * 1",
|
|
1453
|
+
sourceId: "all",
|
|
1454
|
+
allSources: true,
|
|
1455
|
+
limit: 9,
|
|
1456
|
+
sourceTimeoutMs: 45000,
|
|
1457
|
+
minCandidateCount: 3,
|
|
1458
|
+
}),
|
|
1459
|
+
);
|
|
1460
|
+
expect(result).toMatchObject({
|
|
1461
|
+
enabled: true,
|
|
1462
|
+
schedule: "0 8 * * 1",
|
|
1463
|
+
sourceId: "all",
|
|
1464
|
+
allSources: true,
|
|
1465
|
+
limit: 9,
|
|
1466
|
+
sourceTimeoutMs: 45000,
|
|
1467
|
+
minCandidateCount: 3,
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
it("reads persisted recurring dream settings", async () => {
|
|
1472
|
+
mocks.getUserSetting.mockResolvedValue({
|
|
1473
|
+
enabled: true,
|
|
1474
|
+
schedule: "0 8 * * 1",
|
|
1475
|
+
sourceId: "voice",
|
|
1476
|
+
allSources: false,
|
|
1477
|
+
limit: 7,
|
|
1478
|
+
sourceTimeoutMs: 20000,
|
|
1479
|
+
minCandidateCount: 2,
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
await expect(getDreamSettings()).resolves.toMatchObject({
|
|
1483
|
+
enabled: true,
|
|
1484
|
+
schedule: "0 8 * * 1",
|
|
1485
|
+
sourceId: "voice",
|
|
1486
|
+
allSources: false,
|
|
1487
|
+
limit: 7,
|
|
1488
|
+
sourceTimeoutMs: 20000,
|
|
1489
|
+
minCandidateCount: 2,
|
|
1490
|
+
});
|
|
1491
|
+
});
|
|
1492
|
+
});
|