@stigmer/server 3.36.0 → 3.37.1
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/dist/domain/agentexecution/artifacts.d.ts.map +1 -1
- package/dist/domain/agentexecution/artifacts.js +15 -6
- package/dist/domain/agentexecution/artifacts.js.map +1 -1
- package/dist/domain/agentexecution/constants.d.ts +0 -10
- package/dist/domain/agentexecution/constants.d.ts.map +1 -1
- package/dist/domain/agentexecution/constants.js +0 -12
- package/dist/domain/agentexecution/constants.js.map +1 -1
- package/dist/domain/agentexecution/lifecycle.d.ts.map +1 -1
- package/dist/domain/agentexecution/lifecycle.js +5 -3
- package/dist/domain/agentexecution/lifecycle.js.map +1 -1
- package/dist/domain/agentexecution/platform-rows.d.ts +63 -0
- package/dist/domain/agentexecution/platform-rows.d.ts.map +1 -0
- package/dist/domain/agentexecution/platform-rows.js +106 -0
- package/dist/domain/agentexecution/platform-rows.js.map +1 -0
- package/dist/domain/agentexecution/subscribe.d.ts.map +1 -1
- package/dist/domain/agentexecution/subscribe.js +7 -4
- package/dist/domain/agentexecution/subscribe.js.map +1 -1
- package/dist/domain/agentexecution/update-status.d.ts.map +1 -1
- package/dist/domain/agentexecution/update-status.js +48 -17
- package/dist/domain/agentexecution/update-status.js.map +1 -1
- package/dist/domain/agentexecution/usage.d.ts +0 -2
- package/dist/domain/agentexecution/usage.d.ts.map +1 -1
- package/dist/domain/agentexecution/usage.js +12 -10
- package/dist/domain/agentexecution/usage.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +4 -1
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js +30 -8
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/workflow/controller.js +2 -1
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflow/steps.d.ts +7 -4
- package/dist/domain/workflow/steps.d.ts.map +1 -1
- package/dist/domain/workflow/steps.js +29 -17
- package/dist/domain/workflow/steps.js.map +1 -1
- package/dist/domain/workflowexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/workflowexecution/create-steps.js +6 -2
- package/dist/domain/workflowexecution/create-steps.js.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.d.ts.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.js +5 -2
- package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
- package/dist/domain/workflowexecution/send-signal.d.ts.map +1 -1
- package/dist/domain/workflowexecution/send-signal.js +6 -3
- package/dist/domain/workflowexecution/send-signal.js.map +1 -1
- package/dist/domain/workflowexecution/submit-approval.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-approval.js +6 -2
- package/dist/domain/workflowexecution/submit-approval.js.map +1 -1
- package/dist/domain/workflowexecution/submit-file-decision.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-file-decision.js +6 -2
- package/dist/domain/workflowexecution/submit-file-decision.js.map +1 -1
- package/dist/domain/workflowexecution/submit-workflow-task-approval.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-workflow-task-approval.js +6 -2
- package/dist/domain/workflowexecution/submit-workflow-task-approval.js.map +1 -1
- package/dist/domain/workflowexecution/subscribe-events.d.ts.map +1 -1
- package/dist/domain/workflowexecution/subscribe-events.js +6 -2
- package/dist/domain/workflowexecution/subscribe-events.js.map +1 -1
- package/dist/domain/workflowexecution/subscribe.d.ts.map +1 -1
- package/dist/domain/workflowexecution/subscribe.js +7 -4
- package/dist/domain/workflowexecution/subscribe.js.map +1 -1
- package/dist/extensions/organization-directory.d.ts +29 -6
- package/dist/extensions/organization-directory.d.ts.map +1 -1
- package/dist/extensions/organization-directory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts +9 -4
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +10 -12
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/dist/temporal/agentexecution/runner-result.d.ts +12 -0
- package/dist/temporal/agentexecution/runner-result.d.ts.map +1 -1
- package/dist/temporal/agentexecution/runner-result.js +15 -0
- package/dist/temporal/agentexecution/runner-result.js.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +44 -22
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
- package/package.json +6 -6
- package/src/domain/agentexecution/__tests__/agentexecution.test.ts +2 -8
- package/src/domain/agentexecution/__tests__/store-faults.test.ts +315 -0
- package/src/domain/agentexecution/__tests__/update-status.test.ts +217 -0
- package/src/domain/agentexecution/artifacts.ts +21 -12
- package/src/domain/agentexecution/constants.ts +0 -15
- package/src/domain/agentexecution/lifecycle.ts +5 -3
- package/src/domain/agentexecution/platform-rows.ts +133 -0
- package/src/domain/agentexecution/subscribe.ts +7 -3
- package/src/domain/agentexecution/update-status.ts +70 -19
- package/src/domain/agentexecution/usage.ts +16 -13
- package/src/domain/organization/controller.ts +47 -10
- package/src/domain/workflow/__tests__/version-audit-degradation.test.ts +192 -0
- package/src/domain/workflow/controller.ts +2 -1
- package/src/domain/workflow/steps.ts +30 -18
- package/src/domain/workflowexecution/__tests__/store-faults.test.ts +418 -0
- package/src/domain/workflowexecution/create-steps.ts +6 -2
- package/src/domain/workflowexecution/lifecycle.ts +5 -2
- package/src/domain/workflowexecution/send-signal.ts +11 -4
- package/src/domain/workflowexecution/submit-approval.ts +6 -2
- package/src/domain/workflowexecution/submit-file-decision.ts +6 -2
- package/src/domain/workflowexecution/submit-workflow-task-approval.ts +6 -2
- package/src/domain/workflowexecution/subscribe-events.ts +6 -2
- package/src/domain/workflowexecution/subscribe.ts +11 -4
- package/src/extensions/__tests__/extension-composition.test.ts +90 -18
- package/src/extensions/organization-directory.ts +33 -7
- package/src/index.ts +5 -1
- package/src/pipeline/__tests__/blind-not-found.test.ts +297 -0
- package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +67 -5
- package/src/pipeline/steps/guard-memory-capture.ts +24 -19
- package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +51 -1
- package/src/temporal/agentexecution/__tests__/own-behalf-status-writes.test.ts +149 -9
- package/src/temporal/agentexecution/__tests__/replay-histories/runner-failed.json +757 -0
- package/src/temporal/agentexecution/__tests__/replay-histories/user-cancel.json +755 -0
- package/src/temporal/agentexecution/__tests__/replay.test.ts +4 -2
- package/src/temporal/agentexecution/runner-result.ts +16 -0
- package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +49 -22
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
* the ONE report that 404s (it verifies the execution exists).
|
|
14
14
|
*
|
|
15
15
|
* Faithful-port notes:
|
|
16
|
-
* - The unknown-execution message is Go's mangled helper output,
|
|
17
|
-
* byte-for-byte (constants.ts, sub-project DD-001).
|
|
18
16
|
* - Org matching is case-insensitive (Go strings.EqualFold); slug-shaped
|
|
19
17
|
* orgs make the exotic Unicode fold edges unreachable, so toLowerCase
|
|
20
18
|
* comparison sits inside Go's envelope.
|
|
@@ -26,7 +24,6 @@
|
|
|
26
24
|
import { create, fromBinary } from "@bufbuild/protobuf";
|
|
27
25
|
import type { Duration } from "@bufbuild/protobuf/wkt";
|
|
28
26
|
import { DurationSchema } from "@bufbuild/protobuf/wkt";
|
|
29
|
-
import { Code, ConnectError } from "@connectrpc/connect";
|
|
30
27
|
|
|
31
28
|
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
32
29
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
@@ -68,17 +65,21 @@ import type { Logger } from "../../boot/logger.js";
|
|
|
68
65
|
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
69
66
|
import type { ListReadScope } from "../../extensions/list-read-scope.js";
|
|
70
67
|
import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
|
|
71
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
internalError,
|
|
70
|
+
invalidArgumentError,
|
|
71
|
+
notFoundError,
|
|
72
|
+
} from "../../pipeline/errors.js";
|
|
72
73
|
import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
73
74
|
import { newPipeline } from "../../pipeline/pipeline.js";
|
|
74
75
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
75
76
|
import { RequestContext } from "../../pipeline/request-context.js";
|
|
76
77
|
import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
78
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
77
79
|
import type { Store } from "../../store/interface.js";
|
|
78
80
|
import { listIndexInstantOfMillis } from "../../store/list-index.js";
|
|
79
81
|
import type { ListIndexRow } from "../../store/list-index.js";
|
|
80
82
|
|
|
81
|
-
import { executionUsageReportNotFoundMessage } from "./constants.js";
|
|
82
83
|
import { agentExecutionListIndex } from "./list-index.js";
|
|
83
84
|
import { EXECUTION_LIST_KEY, loadAllAgentExecutions } from "./steps.js";
|
|
84
85
|
|
|
@@ -457,9 +458,11 @@ export async function getExecutionUsageReport(
|
|
|
457
458
|
}
|
|
458
459
|
|
|
459
460
|
/**
|
|
460
|
-
* LoadExecution — verifies existence
|
|
461
|
-
*
|
|
462
|
-
*
|
|
461
|
+
* LoadExecution — verifies existence. A missing execution answers the
|
|
462
|
+
* domain's agent_execution NotFound (stigmer/stigmer#859); any other store
|
|
463
|
+
* failure is an infrastructure fault answered as a sanitized Internal, as
|
|
464
|
+
* the approval loads do (submit-approval.ts), never a NotFound that tells
|
|
465
|
+
* the client the execution does not exist.
|
|
463
466
|
*/
|
|
464
467
|
function newLoadExecutionStep(store: Store): PipelineStep<ExecutionReportDesc> {
|
|
465
468
|
return {
|
|
@@ -473,11 +476,11 @@ function newLoadExecutionStep(store: Store): PipelineStep<ExecutionReportDesc> {
|
|
|
473
476
|
executionId,
|
|
474
477
|
AgentExecutionSchema,
|
|
475
478
|
);
|
|
476
|
-
} catch {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (error instanceof ResourceNotFoundError) {
|
|
481
|
+
throw notFoundError("agent_execution", executionId);
|
|
482
|
+
}
|
|
483
|
+
throw internalError(error, "failed to load agent execution");
|
|
481
484
|
}
|
|
482
485
|
ctx.set(EXECUTION_KEY, execution);
|
|
483
486
|
},
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* __tests__/organization.test.ts.
|
|
10
10
|
*
|
|
11
11
|
* getByExternalOrgId is implemented ONLY when the composed
|
|
12
|
-
* OrganizationDirectory provides the lookup (C2, ruling Q7)
|
|
12
|
+
* OrganizationDirectory provides the lookup (C2, ruling Q7). It answers
|
|
13
|
+
* only a caller who may view the identity provider the request names,
|
|
14
|
+
* and resolves the external id within that provider alone (see the
|
|
15
|
+
* handler); with no
|
|
13
16
|
* directory the method stays absent from the partial service
|
|
14
17
|
* implementation and ConnectRPC answers Unimplemented (the SDK
|
|
15
18
|
* capability-probes this and must see UNIMPLEMENTED, not NotFound;
|
|
@@ -32,6 +35,7 @@ import type { FindApiResourcesRequest } from "@stigmer/protos/ai/stigmer/commons
|
|
|
32
35
|
import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
|
|
33
36
|
import { OrganizationQueryController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/query_pb";
|
|
34
37
|
import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
38
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
35
39
|
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
36
40
|
import {
|
|
37
41
|
OrganizationListSchema,
|
|
@@ -61,7 +65,10 @@ import { newPipeline } from "../../pipeline/pipeline.js";
|
|
|
61
65
|
import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
62
66
|
import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
|
|
63
67
|
import { RequestContext } from "../../pipeline/request-context.js";
|
|
64
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
authorizeResolvedResource,
|
|
70
|
+
newAuthorizeStep,
|
|
71
|
+
} from "../../pipeline/steps/authorize.js";
|
|
65
72
|
import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
|
|
66
73
|
import { newBuildNewStateStep } from "../../pipeline/steps/defaults.js";
|
|
67
74
|
import { newBuildUpdateStateStep } from "../../pipeline/steps/build-update-state.js";
|
|
@@ -129,7 +136,7 @@ export function registerOrganizationServices(
|
|
|
129
136
|
// partial implementation and ConnectRPC answers Unimplemented (the
|
|
130
137
|
// capability-probed pin; see the module header).
|
|
131
138
|
const externalLookup =
|
|
132
|
-
deps.organizationDirectory?.
|
|
139
|
+
deps.organizationDirectory?.lookupExternalOrganization?.bind(
|
|
133
140
|
deps.organizationDirectory,
|
|
134
141
|
);
|
|
135
142
|
router.service(OrganizationQueryController, {
|
|
@@ -548,15 +555,22 @@ async function findMyOrganizations(
|
|
|
548
555
|
|
|
549
556
|
/**
|
|
550
557
|
* GetByExternalOrgId — registered only with a directory lookup composed
|
|
551
|
-
* (ruling Q7). The chain stays controller-owned: Authorize
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
558
|
+
* (ruling Q7). The chain stays controller-owned: Authorize (a no-op for
|
|
559
|
+
* this skip-annotated method) → validate → the directory resolves the
|
|
560
|
+
* named identity provider and the external id within it → `can_view` on
|
|
561
|
+
* that provider → LoadTarget-shaped store read.
|
|
562
|
+
*
|
|
563
|
+
* The contract's check is the provider's, so it runs here, on the id the
|
|
564
|
+
* directory resolved mid-chain (`authorizeResolvedResource`, the pattern
|
|
565
|
+
* for skip lanes the annotation cannot express), and before the answer
|
|
566
|
+
* says whether a mapping exists: a caller who may not view the provider
|
|
567
|
+
* learns nothing about its tenants. Every miss arm (no such provider, no
|
|
568
|
+
* mapping, mapped row gone) answers the same NotFound, so an external
|
|
569
|
+
* caller cannot tell a stale mapping from a missing org.
|
|
556
570
|
*/
|
|
557
571
|
async function getByExternalOrgId(
|
|
558
572
|
deps: OrganizationControllerDeps,
|
|
559
|
-
lookup:
|
|
573
|
+
lookup: NonNullable<OrganizationDirectory["lookupExternalOrganization"]>,
|
|
560
574
|
input: OrganizationExternalLookup,
|
|
561
575
|
ctx: HandlerContext,
|
|
562
576
|
): Promise<Organization> {
|
|
@@ -582,7 +596,27 @@ async function getByExternalOrgId(
|
|
|
582
596
|
if (input.externalOrgId === "") {
|
|
583
597
|
throw invalidArgumentError("external_org_id is required");
|
|
584
598
|
}
|
|
585
|
-
const
|
|
599
|
+
const found = await lookup(
|
|
600
|
+
{
|
|
601
|
+
org: input.identityProviderRef?.org ?? "",
|
|
602
|
+
slug: input.identityProviderRef?.slug ?? "",
|
|
603
|
+
},
|
|
604
|
+
input.externalOrgId,
|
|
605
|
+
);
|
|
606
|
+
if (found.kind === "no-identity-provider") {
|
|
607
|
+
throw notFoundError("Organization", input.externalOrgId);
|
|
608
|
+
}
|
|
609
|
+
await authorizeResolvedResource(
|
|
610
|
+
deps.authorizer,
|
|
611
|
+
reqCtx.callerIdentity,
|
|
612
|
+
{
|
|
613
|
+
permission: IamPermission.can_view,
|
|
614
|
+
resourceKind: ApiResourceKind.identity_provider,
|
|
615
|
+
resourceId: found.identityProviderId,
|
|
616
|
+
},
|
|
617
|
+
EXTERNAL_LOOKUP_DENIED_MESSAGE,
|
|
618
|
+
);
|
|
619
|
+
const orgId = found.organizationId;
|
|
586
620
|
if (orgId === undefined) {
|
|
587
621
|
throw notFoundError("Organization", input.externalOrgId);
|
|
588
622
|
}
|
|
@@ -599,3 +633,6 @@ async function getByExternalOrgId(
|
|
|
599
633
|
throw internalError(error, "failed to load organization");
|
|
600
634
|
}
|
|
601
635
|
}
|
|
636
|
+
|
|
637
|
+
/** The lookup's deny copy: the provider's own `get` annotation's words. */
|
|
638
|
+
const EXTERNAL_LOOKUP_DENIED_MESSAGE = "unauthorized to view identity provider";
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins SaveVersionAudit's safe-degradation arms at step level — the
|
|
3
|
+
* composed suite (workflow.test.ts) cannot make the real store fail
|
|
4
|
+
* selectively:
|
|
5
|
+
*
|
|
6
|
+
* - archive (saveAudit) failure clears status.version_hash and
|
|
7
|
+
* metadata.version.id, so the head never references an unresolvable
|
|
8
|
+
* audit entry, and the tag is never assigned;
|
|
9
|
+
* - tag assignment (setAuditTag) failure clears the live
|
|
10
|
+
* metadata.version.tag, so the head never advertises a tag the audit
|
|
11
|
+
* column cannot resolve (stigmer/stigmer#855);
|
|
12
|
+
* - persistOnRevert flushes either revert to the stored row, because on
|
|
13
|
+
* create no Persist step follows this one; without it (update) the
|
|
14
|
+
* chain's own Persist flushes the revert and the step writes nothing;
|
|
15
|
+
* - a failing re-persist is logged, never a failed apply.
|
|
16
|
+
*/
|
|
17
|
+
import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
|
|
18
|
+
import { create } from "@bufbuild/protobuf";
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
|
|
21
|
+
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
22
|
+
import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
23
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
24
|
+
|
|
25
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
26
|
+
import { RequestContext } from "../../../pipeline/request-context.js";
|
|
27
|
+
import { AuditNotFoundError } from "../../../store/interface.js";
|
|
28
|
+
import type { Store } from "../../../store/interface.js";
|
|
29
|
+
import { newSaveVersionAuditStep } from "../steps.js";
|
|
30
|
+
|
|
31
|
+
const silentLogger = createLogger({
|
|
32
|
+
level: "error",
|
|
33
|
+
pretty: false,
|
|
34
|
+
write: () => {},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const HASH = "c".repeat(64);
|
|
38
|
+
|
|
39
|
+
/** What the stored row carried at each re-persist. */
|
|
40
|
+
interface PersistedHead {
|
|
41
|
+
versionHash: string;
|
|
42
|
+
versionId: string;
|
|
43
|
+
tag: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface FakeAuditStore {
|
|
47
|
+
saveAuditError: Error | undefined;
|
|
48
|
+
setAuditTagError: Error | undefined;
|
|
49
|
+
saveResourceError: Error | undefined;
|
|
50
|
+
setAuditTagCalls: number;
|
|
51
|
+
persisted: PersistedHead[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A store exposing ONLY the members the step touches; the cast is the seam. */
|
|
55
|
+
function fakeStore(overrides?: Partial<FakeAuditStore>): {
|
|
56
|
+
store: Store;
|
|
57
|
+
state: FakeAuditStore;
|
|
58
|
+
} {
|
|
59
|
+
const state: FakeAuditStore = {
|
|
60
|
+
saveAuditError: undefined,
|
|
61
|
+
setAuditTagError: undefined,
|
|
62
|
+
saveResourceError: undefined,
|
|
63
|
+
setAuditTagCalls: 0,
|
|
64
|
+
persisted: [],
|
|
65
|
+
...overrides,
|
|
66
|
+
};
|
|
67
|
+
const store = {
|
|
68
|
+
async getAuditByHash(): Promise<never> {
|
|
69
|
+
throw new AuditNotFoundError("not archived");
|
|
70
|
+
},
|
|
71
|
+
async saveAudit(): Promise<void> {
|
|
72
|
+
if (state.saveAuditError !== undefined) {
|
|
73
|
+
throw state.saveAuditError;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async setAuditTag(): Promise<void> {
|
|
77
|
+
state.setAuditTagCalls += 1;
|
|
78
|
+
if (state.setAuditTagError !== undefined) {
|
|
79
|
+
throw state.setAuditTagError;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
async saveResource(
|
|
83
|
+
_k: unknown,
|
|
84
|
+
_id: unknown,
|
|
85
|
+
_schema: unknown,
|
|
86
|
+
wf: Workflow,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
state.persisted.push({
|
|
89
|
+
versionHash: wf.status?.versionHash ?? "",
|
|
90
|
+
versionId: wf.metadata?.version?.id ?? "",
|
|
91
|
+
tag: wf.metadata?.version?.tag ?? "",
|
|
92
|
+
});
|
|
93
|
+
if (state.saveResourceError !== undefined) {
|
|
94
|
+
throw state.saveResourceError;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
} as unknown as Store;
|
|
98
|
+
return { store, state };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function contextWithWorkflow(
|
|
102
|
+
tag: string,
|
|
103
|
+
): RequestContext<typeof WorkflowSchema> {
|
|
104
|
+
return new RequestContext(
|
|
105
|
+
WorkflowSchema,
|
|
106
|
+
create(WorkflowSchema, {
|
|
107
|
+
metadata: {
|
|
108
|
+
id: "wfl_test",
|
|
109
|
+
org: "acme",
|
|
110
|
+
version: { id: HASH, tag },
|
|
111
|
+
},
|
|
112
|
+
status: { versionHash: HASH },
|
|
113
|
+
}),
|
|
114
|
+
testCallerIdentity(),
|
|
115
|
+
ApiResourceKind.workflow,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The create chain's arguments: isCreate, and no Persist follows. */
|
|
120
|
+
function createStep(store: Store) {
|
|
121
|
+
return newSaveVersionAuditStep(store, silentLogger, true, true);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe("SaveVersionAudit — safe degradation", () => {
|
|
125
|
+
it("tag failure on create clears the live tag and re-persists the row tagless", async () => {
|
|
126
|
+
const { store, state } = fakeStore({
|
|
127
|
+
setAuditTagError: new Error("tag column locked"),
|
|
128
|
+
});
|
|
129
|
+
const ctx = contextWithWorkflow("stable");
|
|
130
|
+
|
|
131
|
+
await createStep(store).execute(ctx);
|
|
132
|
+
|
|
133
|
+
expect(ctx.newState.metadata?.version?.tag).toBe("");
|
|
134
|
+
expect(state.persisted).toEqual([
|
|
135
|
+
{ versionHash: HASH, versionId: HASH, tag: "" },
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("tag failure without persistOnRevert clears the live tag and leaves the write to the chain's Persist", async () => {
|
|
140
|
+
const { store, state } = fakeStore({
|
|
141
|
+
setAuditTagError: new Error("tag column locked"),
|
|
142
|
+
});
|
|
143
|
+
const ctx = contextWithWorkflow("stable");
|
|
144
|
+
|
|
145
|
+
await newSaveVersionAuditStep(store, silentLogger, true, false).execute(
|
|
146
|
+
ctx,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(ctx.newState.metadata?.version?.tag).toBe("");
|
|
150
|
+
expect(state.persisted).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("archive failure on create clears the hash and version id, re-persists, and never assigns the tag", async () => {
|
|
154
|
+
const { store, state } = fakeStore({
|
|
155
|
+
saveAuditError: new Error("disk full"),
|
|
156
|
+
});
|
|
157
|
+
const ctx = contextWithWorkflow("stable");
|
|
158
|
+
|
|
159
|
+
await createStep(store).execute(ctx);
|
|
160
|
+
|
|
161
|
+
expect(ctx.newState.status?.versionHash).toBe("");
|
|
162
|
+
expect(ctx.newState.metadata?.version?.id).toBe("");
|
|
163
|
+
expect(state.setAuditTagCalls).toBe(0);
|
|
164
|
+
expect(state.persisted).toEqual([
|
|
165
|
+
{ versionHash: "", versionId: "", tag: "stable" },
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("a failing re-persist after a tag failure is logged, and the step does not throw", async () => {
|
|
170
|
+
const { store, state } = fakeStore({
|
|
171
|
+
setAuditTagError: new Error("tag column locked"),
|
|
172
|
+
saveResourceError: new Error("connection reset"),
|
|
173
|
+
});
|
|
174
|
+
const ctx = contextWithWorkflow("stable");
|
|
175
|
+
|
|
176
|
+
await expect(createStep(store).execute(ctx)).resolves.toBeUndefined();
|
|
177
|
+
|
|
178
|
+
expect(ctx.newState.metadata?.version?.tag).toBe("");
|
|
179
|
+
expect(state.persisted).toHaveLength(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("a clean archive and tag write nothing beyond the audit row", async () => {
|
|
183
|
+
const { store, state } = fakeStore();
|
|
184
|
+
const ctx = contextWithWorkflow("stable");
|
|
185
|
+
|
|
186
|
+
await createStep(store).execute(ctx);
|
|
187
|
+
|
|
188
|
+
expect(ctx.newState.metadata?.version?.tag).toBe("stable");
|
|
189
|
+
expect(state.setAuditTagCalls).toBe(1);
|
|
190
|
+
expect(state.persisted).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -217,7 +217,8 @@ function kindOf(ctx: HandlerContext): ApiResourceKind {
|
|
|
217
217
|
* created via the in-process client AFTER Persist (children need the
|
|
218
218
|
* parent's id), status.default_instance_id lands in an explicit second
|
|
219
219
|
* persist, and v1 archives LAST so the snapshot captures it (the audit
|
|
220
|
-
* step re-persists
|
|
220
|
+
* step re-persists either of its reverts, a stripped hash or a cleared
|
|
221
|
+
* tag, because no Persist follows it here).
|
|
221
222
|
*/
|
|
222
223
|
async function createWorkflow(
|
|
223
224
|
deps: WorkflowControllerDeps,
|
|
@@ -354,10 +354,13 @@ export function newPopulateVersionHashStep(
|
|
|
354
354
|
* duplicate row beats a failed apply.
|
|
355
355
|
*
|
|
356
356
|
* On archive failure the version hash is stripped from the workflow so a
|
|
357
|
-
* set hash always resolves to an audit entry (the revert invariant);
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
357
|
+
* set hash always resolves to an audit entry (the revert invariant); on
|
|
358
|
+
* tag-assignment failure the live tag is cleared so the head never
|
|
359
|
+
* advertises a tag the audit column cannot resolve (stigmer/stigmer#855).
|
|
360
|
+
* persistOnRevert flushes either revert to the stored row, for a chain in
|
|
361
|
+
* which no Persist step follows this one (the create path archives last so
|
|
362
|
+
* default_instance_id is captured; the update path has a Persist step
|
|
363
|
+
* following that flushes the revert itself).
|
|
361
364
|
*/
|
|
362
365
|
export function newSaveVersionAuditStep(
|
|
363
366
|
store: Store,
|
|
@@ -385,6 +388,27 @@ export function newSaveVersionAuditStep(
|
|
|
385
388
|
? ctx.apiResourceKind
|
|
386
389
|
: ApiResourceKind.workflow;
|
|
387
390
|
|
|
391
|
+
// A revert changes the head after the chain's last write, so it
|
|
392
|
+
// reaches the stored row only through this re-persist. A failure here
|
|
393
|
+
// is logged, never a failed apply: the response is already consistent
|
|
394
|
+
// and the next apply rewrites the row.
|
|
395
|
+
const flushRevert = async (revert: string): Promise<void> => {
|
|
396
|
+
if (!persistOnRevert) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
await store.saveResource(kind, workflowId, WorkflowSchema, wf);
|
|
401
|
+
} catch (persistError) {
|
|
402
|
+
logger.error(`failed to re-persist workflow after ${revert}`, {
|
|
403
|
+
error:
|
|
404
|
+
persistError instanceof Error
|
|
405
|
+
? persistError.message
|
|
406
|
+
: String(persistError),
|
|
407
|
+
workflowId,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
388
412
|
let alreadyArchived = false;
|
|
389
413
|
try {
|
|
390
414
|
await store.getAuditByHash(kind, workflowId, versionHash, WorkflowSchema);
|
|
@@ -428,20 +452,7 @@ export function newSaveVersionAuditStep(
|
|
|
428
452
|
wf.metadata.version.id = "";
|
|
429
453
|
}
|
|
430
454
|
ctx.setNewState(wf);
|
|
431
|
-
|
|
432
|
-
if (persistOnRevert) {
|
|
433
|
-
try {
|
|
434
|
-
await store.saveResource(kind, workflowId, WorkflowSchema, wf);
|
|
435
|
-
} catch (persistError) {
|
|
436
|
-
logger.error("failed to re-persist workflow after reverting version hash", {
|
|
437
|
-
error:
|
|
438
|
-
persistError instanceof Error
|
|
439
|
-
? persistError.message
|
|
440
|
-
: String(persistError),
|
|
441
|
-
workflowId,
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
}
|
|
455
|
+
await flushRevert("reverting version hash");
|
|
445
456
|
return;
|
|
446
457
|
}
|
|
447
458
|
}
|
|
@@ -468,6 +479,7 @@ export function newSaveVersionAuditStep(
|
|
|
468
479
|
if (wf.metadata?.version !== undefined) {
|
|
469
480
|
wf.metadata.version.tag = "";
|
|
470
481
|
ctx.setNewState(wf);
|
|
482
|
+
await flushRevert("clearing its tag");
|
|
471
483
|
}
|
|
472
484
|
}
|
|
473
485
|
}
|