mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-25 12:36:30 +00:00
test(e2e): pin the request payload on the mutations that carry one
Seven payload-bearing cassette entries answered success no matter what the app
sent: the four flow mutations, both create mutations, and the mkdir POST. Since
each assertion then reads canned cassette data, a wrong payload — a stale closure
deleting flow 6 while flow 5 is open, an unbound name field — repainted exactly
the asserted UI and the suite stayed green. Verified by flipping a pinned flow id
and a pinned path: both specs now fail on the unmatched request, and both passed
before.
Matching had to grow a level for this: `bodySubset` documented a deep subset but
compared nested values whole, so `{ input: { name } }` demanded the entire input.
It now recurses, which is also what makes the API-token pin possible — its `ttl`
is derived from the clock at submit time and drifts a second between runs, so
only the operator-entered name is pinned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
66fd432edd
commit
a6f867cdae
@@ -17,9 +17,21 @@ const stableStringify = (value: unknown): string =>
|
||||
: node,
|
||||
) ?? 'undefined';
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
// Recurses so a nested pin stays a subset — `{ input: { name } }` must not demand
|
||||
// that the whole `input` match, or any sibling the app also sends (a time-derived
|
||||
// TTL, a default) would make the entry unmatchable.
|
||||
const isSubsetMatch = (expected?: Record<string, unknown>, actual?: Record<string, unknown>): boolean =>
|
||||
!expected ||
|
||||
Object.entries(expected).every(([key, value]) => stableStringify(actual?.[key]) === stableStringify(value));
|
||||
Object.entries(expected).every(([key, value]) => {
|
||||
const found = actual?.[key];
|
||||
|
||||
return isPlainObject(value) && isPlainObject(found)
|
||||
? isSubsetMatch(value, found)
|
||||
: stableStringify(found) === stableStringify(value);
|
||||
});
|
||||
|
||||
const sleep = (delayMs: number) => new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
||||
|
||||
|
||||
@@ -20,7 +20,17 @@ test.describe('api tokens crud', { tag: '@crud' }, () => {
|
||||
|
||||
test.use({
|
||||
cassette: apiTokensCassette({
|
||||
mutations: { createAPIToken: [{ data: created, setFlag: 'token-created' }] },
|
||||
mutations: {
|
||||
createAPIToken: [
|
||||
// `ttl` is derived from the clock at submit time and drifts by a second
|
||||
// between runs, so only the operator-entered name is pinned.
|
||||
{
|
||||
data: created,
|
||||
setFlag: 'token-created',
|
||||
variables: { input: { name: 'E2E created token' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
queries: {
|
||||
apiTokens: [
|
||||
{ data: tokensList(SEED_TOKEN) },
|
||||
|
||||
@@ -44,7 +44,21 @@ test.describe('knowledges crud', { tag: '@crud' }, () => {
|
||||
|
||||
test.use({
|
||||
cassette: knowledgesCassette({
|
||||
mutations: { createKnowledgeDocument: [{ data: created }] },
|
||||
mutations: {
|
||||
createKnowledgeDocument: [
|
||||
{
|
||||
data: created,
|
||||
variables: {
|
||||
input: {
|
||||
answerType: 'other',
|
||||
content: 'E2E knowledge content',
|
||||
docType: 'answer',
|
||||
question: 'What is the E2E answer?',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
queries: { knowledgeDocument: [{ data: detail, variables: { id: '301' } }] },
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -29,7 +29,11 @@ test.describe('resources', { tag: '@coverage' }, () => {
|
||||
cassette: resourcesCassette({
|
||||
rest: {
|
||||
'POST /api/v1/resources/mkdir': [
|
||||
{ body: { data: {}, status: 'success' }, setFlag: 'folder-created' },
|
||||
{
|
||||
body: { data: {}, status: 'success' },
|
||||
bodySubset: { path: 'new-folder' },
|
||||
setFlag: 'folder-created',
|
||||
},
|
||||
],
|
||||
},
|
||||
subscriptions: {
|
||||
|
||||
@@ -26,7 +26,15 @@ test.describe('flow lifecycle', { tag: '@flows' }, () => {
|
||||
|
||||
test.use({
|
||||
cassette: flowsCassette({
|
||||
mutations: { renameFlow: [{ data: renamed, setFlag: 'flow-renamed' }] },
|
||||
mutations: {
|
||||
renameFlow: [
|
||||
{
|
||||
data: renamed,
|
||||
setFlag: 'flow-renamed',
|
||||
variables: { flowId: '5', title: 'E2E Alpha Renamed' },
|
||||
},
|
||||
],
|
||||
},
|
||||
subscriptions: {
|
||||
flowUpdated: [
|
||||
{
|
||||
@@ -60,7 +68,7 @@ test.describe('flow lifecycle', { tag: '@flows' }, () => {
|
||||
|
||||
test.use({
|
||||
cassette: flowsCassette({
|
||||
mutations: { finishFlow: [{ data: finished, setFlag: 'flow-finished' }] },
|
||||
mutations: { finishFlow: [{ data: finished, setFlag: 'flow-finished', variables: { flowId: '5' } }] },
|
||||
subscriptions: {
|
||||
flowUpdated: [
|
||||
{
|
||||
@@ -101,7 +109,9 @@ test.describe('flow lifecycle', { tag: '@flows' }, () => {
|
||||
// asserts below before the frame lands.
|
||||
test.use({
|
||||
cassette: flowsCassette({
|
||||
mutations: { addFavoriteFlow: [{ data: favorited, setFlag: 'flow-favorited' }] },
|
||||
mutations: {
|
||||
addFavoriteFlow: [{ data: favorited, setFlag: 'flow-favorited', variables: { flowId: '5' } }],
|
||||
},
|
||||
subscriptions: {
|
||||
settingsUserUpdated: [
|
||||
{
|
||||
@@ -146,7 +156,7 @@ test.describe('flow lifecycle', { tag: '@flows' }, () => {
|
||||
|
||||
test.use({
|
||||
cassette: flowsCassette({
|
||||
mutations: { deleteFlow: [{ data: deleted, setFlag: 'flow-deleted' }] },
|
||||
mutations: { deleteFlow: [{ data: deleted, setFlag: 'flow-deleted', variables: { flowId: '5' } }] },
|
||||
subscriptions: {
|
||||
flowDeleted: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user